Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ jobs:
strategy:
fail-fast: false
matrix:
php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
name: PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Install PHP
uses: shivammathur/setup-php@master
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: parallel
- name: Validate composer.json and composer.lock
run: composer validate
- name: Get Composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
Expand Down
24 changes: 15 additions & 9 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* php-cs file
* contains rubric for code-style fixes
*
*
* @package chemem/asyncify
* @author Lochemem Bruno Michael
* @license Apache-2.0
Expand All @@ -15,18 +15,24 @@
use PhpCsFixer\Finder;

$finder = Finder::create()
->exclude(['vendor', 'cache', 'bin'])
->exclude(['vendor', 'cache'])
->in(__DIR__);

$config = new Config;

return $config
->setRules([
'@PSR12' => true,
'linebreak_after_opening_tag' => true,
'binary_operator_spaces' => [
'operators' => ['=>' => 'align', '=' => 'align'],
],
])
->setRules(
[
'@PSR12' => true,
'linebreak_after_opening_tag' => true,
'trailing_comma_in_multiline' => false,
'binary_operator_spaces' => [
'operators' => [
'=>' => 'align',
'=' => 'align',
],
],
]
)
->setFinder($finder)
->setIndent(' ');
68 changes: 56 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@
[![StyleCI](https://github.styleci.io/repos/365018048/shield?branch=master)](https://github.styleci.io/repos/365018048?branch=master)
[![asyncify CI](https://github.com/ace411/asyncify/actions/workflows/ci.yml/badge.svg)](https://github.com/ace411/asyncify/actions/workflows/ci.yml)
[![License](http://poser.pugx.org/chemem/asyncify/license)](https://packagist.org/packages/chemem/asyncify)
[![composer.lock](http://poser.pugx.org/chemem/asyncify/composerlock)](https://packagist.org/packages/chemem/asyncify)
[![Dependents](http://poser.pugx.org/chemem/asyncify/dependents)](https://packagist.org/packages/chemem/asyncify)
[![Latest Stable Version](http://poser.pugx.org/chemem/asyncify/v)](https://packagist.org/packages/chemem/asyncify)
[![PHP Version Require](http://poser.pugx.org/chemem/asyncify/require/php)](https://packagist.org/packages/chemem/asyncify)

</span>

A simple PHP library that runs your synchronous PHP functions asynchronously.
A simple library with which to run blocking I/O in a non-blocking fashion.

## Requirements

- PHP 7.2 or higher
- PHP 7.2 or newer

## Rationale

PHP is a largely synchronous (blocking) runtime. Asynchrony - achievable via ReactPHP and other similar suites - is a potent approach to mitigating the arduousness of I/O operations that feature prominently in day-to-day programming. Melding blocking and non-blocking routines in PHP can be a tricky proposition: when attempted haphazardly, it can yield unsightly outcomes.

The impetus for creating and maintaining `asyncify` is combining blocking and non-blocking PHP. Built atop ReactPHP, `asyncify` is a tool that allows one to run blocking PHP functions in an event-driven I/O environment.
PHP is home to a host of functions that condition CPU idleness between successive (serial) executions—blocking functions. The expense of blocking calls—invocations of such functions—is such that they can, when deployed haphazardly in evented systems, inflict unnecessary CPU waiting behavior whilst the kernel attempts to interleave non-blocking calls. `asyncify` is a bridge between the blocking I/O in the language userspace and the evented I/O in ReactPHP. It allows those who choose to avail themselves of it the ability to run their blocking code, with minimal plumbing, in evented systems, without derailing them.

## Installation

Expand All @@ -31,6 +28,14 @@ Though it is possible to clone the repo, Composer remains the best tool for inst
$ composer require chemem/asyncify
```

Newer versions of the library prioritize multithreading. The precondition for operationalizing multithreading is installing the [parallel](https://github.com/krakjoe/parallel) extension (`ext-parallel`) and [`react-parallel/runtime`](https://github.com/reactphp-parallel/runtime) library which can be done with the directives in the snippet below.

```sh
$ pie install pecl/parallel
$ echo "\nextension=parallel" >> "/path/to/php.ini"
$ composer require react-parallel/runtime
```

## Usage

If you want to take a Functional Programming approach, facilitated by currying, the example below should suffice.
Expand Down Expand Up @@ -74,7 +79,33 @@ The examples directory contains more nuanced uses of the library that I recommen

- `asyncify` is no panacea, but is capable of asynchronously executing a plethora of blocking calls. As presently constituted, the library is **incapable of processing inputs and outputs that cannot be serialized**. Its quintessential asynchronous function application primitive - `call()` - works almost exclusively with string encodings of native language functions and lambdas imported via an autoloading mechanism.

- The library cannot parse closures. All executable arbitrary code should be emplaced in a string whose sole constituent is an immediately invokable anonymous function the format of which is `(function (...$args) { /* signature */ })`.
- The library, in its default configuration, cannot parse closures. All executable arbitrary code should be emplaced in a string whose sole constituent is an immediately invokable anonymous function the format of which is `(function (...$args) { /* signature */ })`.

## Multithreading

With multithreading enabled, it is possible to invoke closures and other lambdas without necessarily representing them as strings. Although string encodings are still workable, lambdas like closures should be the preferred option for representing arbitrary blocking logic. The code in the following example should work with multithreading enabled.

```php
use function Chemem\Asyncify\call;

$exec = call(
function (...$args) {
return \file_get_contents(...$args);
},
['/path/to/file']
);

$exec->then(
function (string $contents) {
echo $contents;
},
function (\Throwable $err) {
echo $err->getMessage();
}
);
```

> It must be noted that string representations of lambdas (anonymous functions, closures and such) that are compatible with the default child process configuration, are not usable in versions that support multithreading.

## API Reference

Expand All @@ -83,11 +114,16 @@ The examples directory contains more nuanced uses of the library that I recommen
```php
namespace Chemem\Asyncify;

use React\{
EventLoop\LoopInterface,
Promise\PromiseInterface,
};

class Async {

/* Methods */
public static create( ?string $autoload = null [, ?React\EventLoop\LoopInterface $rootDir = null ] ) : Async;
public function call( string $function [, array $args ] ) : React\Promise\PromiseInterface;
public static create( ?string $autoload = null [, ?LoopInterface $rootDir = null ] ) : Async;
public function call( string|callable $function [, array $args ] ) : PromiseInterface;
}
```

Expand All @@ -100,12 +136,20 @@ class Async {
```php
namespace Chemem\Asyncify;

call ( string $func [, array $args [, ?string $autoload = null [, ?React\EventLoop\LoopInterface $args = null ] ] ] ) : React\Promise\PromiseInterface;
use React\{
EventLoop\LoopInterface,
Promise\PromiseInterface,
};

call ( string|callable $func [, array $args [, ?string $autoload = null [, ?LoopInterface $args = null ] ] ] ) : PromiseInterface;
```

`call` - Curryied function that bootstraps asynchronous function calls

> **Note:** `asyncify` utilizes the autoload file in the root directory of the project from which it is invoked.
### Important Considerations

- `asyncify`, by default, utilizes the autoload file (`autoload.php`) in the `vendor` directory of the composer project in which it resides.
- The library converts all errors in the functions slated for non-blocking execution to exceptions.

## Dealing with problems

Expand Down
19 changes: 11 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chemem/asyncify",
"description": "A package that runs synchronous PHP functions asynchronously.",
"description": "A simple library with which to run blocking I/O in a non-blocking fashion.",
"license": "Apache-2.0",
"type": "library",
"authors": [
Expand All @@ -12,15 +12,18 @@
],
"require": {
"php": ">=7.2",
"chemem/bingo-functional": "~2",
"react/child-process": "~0",
"react/promise": "~2"
"react/child-process": "^0",
"react/promise": "^2 || ^3"
},
"require-dev": {
"ergebnis/composer-normalize": "~2",
"friendsofphp/php-cs-fixer": "~2 || ~3",
"phpunit/phpunit": "~8 || ~9",
"react/async": "~3 || ~4"
"ergebnis/composer-normalize": "^2",
"friendsofphp/php-cs-fixer": "^2 || ^3",
"phpunit/phpunit": "^8 || ^9",
"react/async": "^3 || ^4"
},
"suggest": {
"ext-parallel": "A succinct parallel concurrency API for PHP 8",
"react-parallel/runtime": "Convenience wrapper around the ext-parallel Runtime and ReactPHP"
},
"minimum-stability": "stable",
"autoload": {
Expand Down
7 changes: 5 additions & 2 deletions examples/readFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@

$call = call('file_get_contents', [])
->then(
function (?int $result) {
echo \sprintf("Wrote %d bytes\n", $result);
function (?string $result) {
echo \sprintf(
"Read %d bytes\n",
\strlen($result)
);
},
function (\Throwable $err) {
echo $err->getMessage() . PHP_EOL;
Expand Down
60 changes: 53 additions & 7 deletions src/Async.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@

use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;
use ReactParallel\EventLoop\EventLoopBridge;
use ReactParallel\Runtime\Runtime;

use function Chemem\Asyncify\Internal\asyncify;
use function Chemem\Asyncify\Internal\thread;
use function Chemem\Asyncify\Internal\Functional\filepath;

use const Chemem\Asyncify\Internal\PHP_THREADABLE;

class Async
{
Expand All @@ -33,10 +39,31 @@ class Async
*/
private $autoload;

/**
* Runtime object
*
* @var Runtime $runtime
*/
private $runtime;

public function __construct(?string $autoload = null, ?LoopInterface $loop = null)
{
$this->loop = $loop;
$this->autoload = $autoload;

if (PHP_THREADABLE) {
$this->runtime = new Runtime(
new EventLoopBridge($this->loop),
$this->autoload ?? filepath(0, 'vendor/autoload.php')
);
}
}

public function __destruct()
{
if (isset($this->runtime)) {
$this->runtime->close();
}
}

/**
Expand All @@ -49,18 +76,20 @@ public function __construct(?string $autoload = null, ?LoopInterface $loop = nul
* @param string $autoload
* @return Async
*/
public static function create(?string $autoload = null, ?LoopInterface $loop = null): Async
{
public static function create(
?string $autoload = null,
?LoopInterface $loop = null
): Async {
return new static($autoload, $loop);
}

/**
* call
* asynchronously calls a synchronous PHP function and subsumes result in promise
*
* call :: String -> Array -> Promise s a
* call :: Sum String (a -> b) -> Array -> Promise s b
*
* @param string $function
* @param string|callable $function
* @param array $args
* @return PromiseInterface
* @example
Expand All @@ -75,11 +104,28 @@ public static function create(?string $autoload = null, ?LoopInterface $loop = n
* function (Throwable $err) {
* echo $err->getMessage() . PHP_EOL;
* }
* )
* );
* => file_get_contents(/path/to/file): Failed to open stream: No such file or directory
*/
public function call(string $function, array $args): PromiseInterface
public function call($function, array $args): PromiseInterface
{
return asyncify($function, $args, $this->autoload, $this->loop);
$params = [$function, $args];

return PHP_THREADABLE ?
thread(
...\array_merge(
$params,
[$this->runtime]
)
) :
asyncify(
...\array_merge(
$params,
[
$this->autoload,
$this->loop
]
)
);
}
}
Loading
Loading