diff --git a/README.md b/README.md index 8fbe641..d5ee493 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,17 @@ -# NOTE: This package is no longer maintained. Use [react/promise](https://github.com/reactphp/promise) instead! - # Async -Async utilities for [ReactPHP](https://reactphp.org/). - -It is heavily influenced by [async.js](https://github.com/caolan/async). - [![CI status](https://github.com/reactphp/async/workflows/CI/badge.svg)](https://github.com/reactphp/async/actions) -This library allows you to manage async control flow. It provides a number of -combinators for continuation-passing style (aka callbacks). Instead of nesting -those callbacks, you can declare them as a list, which is resolved -sequentially in an async manner. +Async utilities for [ReactPHP](https://reactphp.org/). +This library allows you to manage async control flow. It provides a number of +combinators for [Promise](https://github.com/reactphp/promise)-based APIs. +Instead of nesting or chaining promise callbacks, you can declare them as a +list, which is resolved sequentially in an async manner. React/Async will not automagically change blocking code to be async. You need to have an actual event loop and non-blocking libraries interacting with that -event loop for it to work. You can use `react/event-loop` for this, but you -don't have to. As long as you have a callback-based API that runs in an event -loop, it can be used with this library. - -*You must be running inside an event loop for react/async to make any sense -whatsoever!* +event loop for it to work. As long as you have a Promise-based API that runs in +an event loop, it can be used with this library. **Table of Contents** @@ -62,112 +53,116 @@ Async\parallel(…); ### parallel() -The `parallel(array $tasks, ?callable $callback = null, ?callable $errback = null): void` function can be used +The `parallel(array> $tasks): PromiseInterface,Exception>` function can be used like this: ```php then(function (array $results) { + foreach ($results as $result) { + var_dump($result); } -); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); ``` ### series() -The `series(array $tasks, ?callable $callback = null, ?callable $errback = null): void` function can be used +The `series(array> $tasks): PromiseInterface,Exception>` function can be used like this: ```php then(function (array $results) { + foreach ($results as $result) { + var_dump($result); } -); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); ``` ### waterfall() -The `waterfall(array $tasks, ?callable $callback = null, ?callable $errback = null): void` function can be used +The `waterfall(array> $tasks): PromiseInterface` function can be used like this: ```php then(function ($prev) { + echo "Final result is $prev\n"; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); ``` ## Todo @@ -210,3 +205,5 @@ $ php vendor/bin/phpunit ## License MIT, see [LICENSE file](LICENSE). + +This project is heavily influenced by [async.js](https://github.com/caolan/async). diff --git a/composer.json b/composer.json index 44c37fd..10cdd53 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ } ], "require": { - "php": ">=5.3.2" + "php": ">=5.3.2", + "react/promise": "^2.8 || ^1.2.1" }, "require-dev": { "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", diff --git a/src/functions.php b/src/functions.php index b717cd8..5a9dc22 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,35 +2,32 @@ namespace React\Async; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; + /** - * @param array $tasks - * @param ?callable $callback - * @param ?callable $errback - * @return void + * @param array> $tasks + * @return PromiseInterface,Exception> */ -function parallel(array $tasks, $callback = null, $errback = null) +function parallel(array $tasks) { + $deferred = new Deferred(); $results = array(); $errors = array(); - $done = function () use (&$results, &$errors, $callback, $errback) { - if (!$callback) { - return; - } - + $done = function () use (&$results, &$errors, $deferred) { if (count($errors)) { - $errback(array_shift($errors)); + $deferred->reject(array_shift($errors)); return; } - $callback($results); + $deferred->resolve($results); }; $numTasks = count($tasks); if (0 === $numTasks) { $done(); - return; } $checkDone = function () use (&$results, &$errors, $numTasks, $done) { @@ -50,18 +47,22 @@ function parallel(array $tasks, $callback = null, $errback = null) $checkDone(); }; - call_user_func($task, $taskCallback, $taskErrback); + $promise = call_user_func($task); + assert($promise instanceof PromiseInterface); + + $promise->then($taskCallback, $taskErrback); } + + return $deferred->promise(); } /** - * @param array $tasks - * @param ?callable $callback - * @param ?callable $errback - * @return void + * @param array> $tasks + * @return PromiseInterface,Exception> */ -function series(array $tasks, $callback = null, $errback = null) +function series(array $tasks) { + $deferred = new Deferred(); $results = array(); /** @var callable():void $next */ @@ -70,53 +71,47 @@ function series(array $tasks, $callback = null, $errback = null) $next(); }; - $done = function () use (&$results, $callback) { - if ($callback) { - call_user_func($callback, $results); - } - }; - - $next = function () use (&$tasks, $taskCallback, $errback, $done) { + $next = function () use (&$tasks, $taskCallback, $deferred, &$results) { if (0 === count($tasks)) { - $done(); + $deferred->resolve($results); return; } $task = array_shift($tasks); - call_user_func($task, $taskCallback, $errback); + $promise = call_user_func($task); + assert($promise instanceof PromiseInterface); + + $promise->then($taskCallback, array($deferred, 'reject')); }; $next(); + + return $deferred->promise(); } /** - * @param array $tasks - * @param ?callable $callback - * @param ?callable $errback - * @return void + * @param array> $tasks + * @return PromiseInterface */ -function waterfall(array $tasks, $callback = null, $errback = null) +function waterfall(array $tasks) { - $taskCallback = function () use (&$next) { - call_user_func_array($next, func_get_args()); - }; + $deferred = new Deferred(); - $done = function () use ($callback) { - if ($callback) { - call_user_func_array($callback, func_get_args()); - } - }; - - $next = function () use (&$tasks, $taskCallback, $errback, $done) { + /** @var callable $next */ + $next = function ($value = null) use (&$tasks, &$next, $deferred) { if (0 === count($tasks)) { - call_user_func_array($done, func_get_args()); + $deferred->resolve($value); return; } $task = array_shift($tasks); - $args = array_merge(func_get_args(), array($taskCallback, $errback)); - call_user_func_array($task, $args); + $promise = call_user_func_array($task, func_get_args()); + assert($promise instanceof PromiseInterface); + + $promise->then($next, array($deferred, 'reject')); }; $next(); + + return $deferred->promise(); } diff --git a/tests/ParallelTest.php b/tests/ParallelTest.php index 8889db2..f27a73e 100644 --- a/tests/ParallelTest.php +++ b/tests/ParallelTest.php @@ -4,6 +4,7 @@ use React; use React\EventLoop\Loop; +use React\Promise\Promise; class ParallelTest extends TestCase { @@ -11,31 +12,33 @@ public function testParallelWithoutTasks() { $tasks = array(); - $callback = $this->expectCallableOnceWith(array()); - $errback = $this->expectCallableNever(); + $promise = React\Async\parallel($tasks); - React\Async\parallel($tasks, $callback, $errback); + $promise->then($this->expectCallableOnceWith(array())); } public function testParallelWithTasks() { $tasks = array( - function ($callback, $errback) { - Loop::addTimer(0.1, function () use ($callback) { - $callback('foo'); + function () { + return new Promise(function ($resolve) { + Loop::addTimer(0.1, function () use ($resolve) { + $resolve('foo'); + }); }); }, - function ($callback, $errback) { - Loop::addTimer(0.1, function () use ($callback) { - $callback('bar'); + function () { + return new Promise(function ($resolve) { + Loop::addTimer(0.1, function () use ($resolve) { + $resolve('bar'); + }); }); }, ); - $callback = $this->expectCallableOnceWith(array('foo', 'bar')); - $errback = $this->expectCallableNever(); + $promise = React\Async\parallel($tasks); - React\Async\parallel($tasks, $callback, $errback); + $promise->then($this->expectCallableOnceWith(array('foo', 'bar'))); $timer = new Timer($this); $timer->start(); @@ -51,24 +54,28 @@ public function testParallelWithError() $called = 0; $tasks = array( - function ($callback, $errback) use (&$called) { - $callback('foo'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('foo'); + }); }, - function ($callback, $errback) { - $e = new \RuntimeException('whoops'); - $errback($e); + function () { + return new Promise(function () { + throw new \RuntimeException('whoops'); + }); }, - function ($callback, $errback) use (&$called) { - $callback('bar'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('bar'); + }); }, ); - $callback = $this->expectCallableNever(); - $errback = $this->expectCallableOnce(); + $promise = React\Async\parallel($tasks); - React\Async\parallel($tasks, $callback, $errback); + $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('whoops'))); $this->assertSame(2, $called); } @@ -78,26 +85,30 @@ public function testParallelWithDelayedError() $called = 0; $tasks = array( - function ($callback, $errback) use (&$called) { - $callback('foo'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('foo'); + }); }, - function ($callback, $errback) { - Loop::addTimer(0.001, function () use ($errback) { - $e = new \RuntimeException('whoops'); - $errback($e); + function () { + return new Promise(function ($_, $reject) { + Loop::addTimer(0.001, function () use ($reject) { + $reject(new \RuntimeException('whoops')); + }); }); }, - function ($callback, $errback) use (&$called) { - $callback('bar'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('bar'); + }); }, ); - $callback = $this->expectCallableNever(); - $errback = $this->expectCallableOnce(); + $promise = React\Async\parallel($tasks); - React\Async\parallel($tasks, $callback, $errback); + $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('whoops'))); Loop::run(); diff --git a/tests/SeriesTest.php b/tests/SeriesTest.php index 7a6bb1b..2e168c2 100644 --- a/tests/SeriesTest.php +++ b/tests/SeriesTest.php @@ -4,6 +4,7 @@ use React; use React\EventLoop\Loop; +use React\Promise\Promise; class SeriesTest extends TestCase { @@ -11,31 +12,33 @@ public function testSeriesWithoutTasks() { $tasks = array(); - $callback = $this->expectCallableOnceWith(array()); - $errback = $this->expectCallableNever(); + $promise = React\Async\series($tasks); - React\Async\series($tasks, $callback, $errback); + $promise->then($this->expectCallableOnceWith(array())); } public function testSeriesWithTasks() { $tasks = array( - function ($callback, $errback) { - Loop::addTimer(0.05, function () use ($callback) { - $callback('foo'); + function () { + return new Promise(function ($resolve) { + Loop::addTimer(0.05, function () use ($resolve) { + $resolve('foo'); + }); }); }, - function ($callback, $errback) { - Loop::addTimer(0.05, function () use ($callback) { - $callback('bar'); + function () { + return new Promise(function ($resolve) { + Loop::addTimer(0.05, function () use ($resolve) { + $resolve('bar'); + }); }); }, ); - $callback = $this->expectCallableOnceWith(array('foo', 'bar')); - $errback = $this->expectCallableNever(); + $promise = React\Async\series($tasks); - React\Async\series($tasks, $callback, $errback); + $promise->then($this->expectCallableOnceWith(array('foo', 'bar'))); $timer = new Timer($this); $timer->start(); @@ -51,24 +54,28 @@ public function testSeriesWithError() $called = 0; $tasks = array( - function ($callback, $errback) use (&$called) { - $callback('foo'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('foo'); + }); }, - function ($callback, $errback) { - $e = new \RuntimeException('whoops'); - $errback($e); + function () { + return new Promise(function () { + throw new \RuntimeException('whoops'); + }); }, - function ($callback, $errback) use (&$called) { - $callback('bar'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('bar'); + }); }, ); - $callback = $this->expectCallableNever(); - $errback = $this->expectCallableOnce(); + $promise = React\Async\series($tasks); - React\Async\series($tasks, $callback, $errback); + $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('whoops'))); $this->assertSame(1, $called); } diff --git a/tests/WaterfallTest.php b/tests/WaterfallTest.php index c8711f8..a04ad5c 100644 --- a/tests/WaterfallTest.php +++ b/tests/WaterfallTest.php @@ -4,6 +4,7 @@ use React; use React\EventLoop\Loop; +use React\Promise\Promise; class WaterfallTest extends TestCase { @@ -11,36 +12,40 @@ public function testWaterfallWithoutTasks() { $tasks = array(); - $callback = $this->expectCallableOnce(); - $errback = $this->expectCallableNever(); + $promise = React\Async\waterfall($tasks); - React\Async\waterfall($tasks, $callback, $errback); + $promise->then($this->expectCallableOnceWith(null)); } public function testWaterfallWithTasks() { $tasks = array( - function ($callback, $errback) { - Loop::addTimer(0.05, function () use ($callback) { - $callback('foo'); + function ($foo = 'foo') { + return new Promise(function ($resolve) use ($foo) { + Loop::addTimer(0.05, function () use ($resolve, $foo) { + $resolve($foo); + }); }); }, - function ($foo, $callback, $errback) { - Loop::addTimer(0.05, function () use ($callback, $foo) { - $callback($foo.'bar'); + function ($foo) { + return new Promise(function ($resolve) use ($foo) { + Loop::addTimer(0.05, function () use ($resolve, $foo) { + $resolve($foo . 'bar'); + }); }); }, - function ($bar, $callback, $errback) { - Loop::addTimer(0.05, function () use ($callback, $bar) { - $callback($bar.'baz'); + function ($bar) { + return new Promise(function ($resolve) use ($bar) { + Loop::addTimer(0.05, function () use ($resolve, $bar) { + $resolve($bar . 'baz'); + }); }); }, ); - $callback = $this->expectCallableOnceWith('foobarbaz'); - $errback = $this->expectCallableNever(); + $promise = React\Async\waterfall($tasks); - React\Async\waterfall($tasks, $callback, $errback); + $promise->then($this->expectCallableOnceWith('foobarbaz')); $timer = new Timer($this); $timer->start(); @@ -56,24 +61,28 @@ public function testWaterfallWithError() $called = 0; $tasks = array( - function ($callback, $errback) use (&$called) { - $callback('foo'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('foo'); + }); }, - function ($foo, $callback, $errback) { - $e = new \RuntimeException('whoops'); - $errback($e); + function ($foo) { + return new Promise(function () { + throw new \RuntimeException('whoops'); + }); }, - function ($callback, $errback) use (&$called) { - $callback('bar'); + function () use (&$called) { $called++; + return new Promise(function ($resolve) { + $resolve('bar'); + }); }, ); - $callback = $this->expectCallableNever(); - $errback = $this->expectCallableOnce(); + $promise = React\Async\waterfall($tasks); - React\Async\waterfall($tasks, $callback, $errback); + $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('whoops'))); $this->assertSame(1, $called); }