├── examples ├── data │ ├── first.log │ ├── second.log │ └── tiny.dat ├── ok │ ├── await.php │ ├── delay.php │ ├── all.php │ ├── race.php │ ├── stream-read.php │ ├── some.php │ ├── throttle.php │ ├── yield-from.php │ ├── wrap.php │ ├── socket.php │ └── read.php └── not-ok │ ├── pipes.php │ └── http.php ├── .gitignore ├── .bootstrap.atoum.php ├── .travis.yml ├── src ├── pipe │ └── functions.php ├── pipe.php ├── wrapper.php ├── time │ └── functions.php ├── socket.php ├── runtime │ └── functions.php ├── loop │ └── functions.php ├── loop.php ├── time.php ├── stream.php ├── process.php └── runtime.php ├── couscous.yml ├── docs ├── time.md ├── README.md └── runtime.md ├── tests ├── func.php └── units │ └── src │ ├── runtime │ └── functions │ │ ├── await.php │ │ ├── race.php │ │ ├── all.php │ │ └── some.php │ ├── time │ └── functions │ │ └── delay.php │ ├── time.php │ └── runtime.php ├── .atoum.php ├── LICENSE ├── composer.json ├── README.md └── .php_cs /examples/data/first.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/data/second.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/data/tiny.dat: -------------------------------------------------------------------------------- 1 | abc 2 | def 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | coverage/ 3 | .couscous/ 4 | 5 | composer.lock 6 | -------------------------------------------------------------------------------- /.bootstrap.atoum.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\pipe; 15 | 16 | require_once __DIR__ . '/../pipe.php'; 17 | 18 | use jubianchi\async\pipe; 19 | 20 | function make() : pipe 21 | { 22 | return new pipe(); 23 | } 24 | -------------------------------------------------------------------------------- /examples/ok/all.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\runtime\tests; 13 | 14 | use mageekguy\atoum; 15 | 16 | abstract class func extends atoum\test 17 | { 18 | public function getTestedClassName() 19 | { 20 | return 'stdClass'; 21 | } 22 | 23 | public function getTestedClassNamespace() 24 | { 25 | return 'jubianchi\\async'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/ok/race.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\runtime\{await, all}; 15 | use function jubianchi\async\stream\{read}; 16 | 17 | var_dump( 18 | await( 19 | all( 20 | read(fopen(__DIR__ . '/data/tiny.dat', 'r'), function($d) { var_dump($d); }, 1), 21 | read(fopen(__DIR__ . '/data/d1.dat', 'r'), function($d) { var_dump($d); }, 10) 22 | ) 23 | ) 24 | ); 25 | -------------------------------------------------------------------------------- /src/pipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async; 15 | 16 | require_once __DIR__ . '/runtime.php'; 17 | 18 | final class pipe extends \splQueue 19 | { 20 | public function __invoke($data) 21 | { 22 | $this->enqueue($data); 23 | } 24 | 25 | public function dequeue() : \generator 26 | { 27 | while ($this->count() === 0) { 28 | yield; 29 | } 30 | 31 | return parent::dequeue(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/ok/some.php: -------------------------------------------------------------------------------- 1 | addWriter(new \mageekguy\atoum\writers\std\out()); 9 | $coverage->setOutPutDirectory(__DIR__ . '/coverage'); 10 | 11 | $telemetry = new reports\telemetry(); 12 | $telemetry->addWriter(new std\out()); 13 | $telemetry->readProjectNameFromComposerJson(__DIR__ . DIRECTORY_SEPARATOR . 'composer.json'); 14 | 15 | $runner 16 | ->addExtension(new reports\extension($script)) 17 | ->addReport($coverage) 18 | ->enableBranchesAndPathsCoverage() 19 | ->addReport($telemetry) 20 | ; 21 | 22 | $script 23 | ->addTestsFromDirectory(__DIR__ . '/tests/units') 24 | ->addDefaultReport() 25 | ; 26 | -------------------------------------------------------------------------------- /examples/ok/throttle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\runtime\{await, race}; 15 | use function jubianchi\async\time\{delay, throttle}; 16 | 17 | $start = microtime(true); 18 | 19 | await( 20 | race( 21 | delay(3000), 22 | throttle(500, function() { var_dump(__LINE__); }), 23 | throttle(1000, function() { var_dump(__LINE__); }) 24 | ) 25 | ); 26 | 27 | echo 'Time spent: ' . ($with = microtime(true) - $start) . PHP_EOL; 28 | -------------------------------------------------------------------------------- /src/wrapper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async; 15 | 16 | final class wrapper 17 | { 18 | private $wrapped; 19 | 20 | private function __construct(\generator $value) 21 | { 22 | $this->wrapped = $value; 23 | } 24 | 25 | public static function unwrap(wrapper $wrapper) : \generator 26 | { 27 | return $wrapper->wrapped; 28 | } 29 | 30 | public static function wrap(\generator $value) 31 | { 32 | return new self($value); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/not-ok/pipes.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\pipe\{make}; 15 | use function jubianchi\async\runtime\{await, all, fork}; 16 | use function jubianchi\async\time\{delay, throttle}; 17 | 18 | 19 | $pipe = make(); 20 | $i = 0; 21 | await( 22 | all( 23 | throttle(2500, function() use ($pipe, &$i) { $pipe->enqueue($i++); }), 24 | throttle(500, function() use ($pipe) { echo __LINE__; var_dump("\033[32m" . (yield from $pipe->dequeue()) . "\033[0m"); }), 25 | throttle(1000, function() use ($pipe) { echo __LINE__; var_dump("\033[31m" . (yield from $pipe->dequeue()) . "\033[0m"); }) 26 | ) 27 | ); 28 | -------------------------------------------------------------------------------- /src/time/functions.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\time; 15 | 16 | require_once __DIR__ . '/../time.php'; 17 | 18 | use jubianchi\async\time; 19 | 20 | /** 21 | * @api 22 | * 23 | * @param int $timeout 24 | * @param mixed $resolve 25 | * 26 | * @return \generator 27 | */ 28 | function delay(int $timeout, $resolve = null) : \generator 29 | { 30 | return time::delay($timeout, $resolve); 31 | } 32 | 33 | /** 34 | * @api 35 | * 36 | * @param int $interval 37 | * @param $resolve 38 | * 39 | * @return \generator 40 | */ 41 | function throttle(int $interval, $resolve) : \generator 42 | { 43 | return time::throttle($interval, $resolve); 44 | } 45 | -------------------------------------------------------------------------------- /examples/ok/yield-from.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\runtime\{await, all, wrap}; 15 | use function jubianchi\async\time\{delay}; 16 | 17 | $start = microtime(true); 18 | 19 | function second() { 20 | var_dump(__FUNCTION__ . ' - ' . 3); 21 | yield from delay(1000); 22 | var_dump(__FUNCTION__ . ' - ' . 4); 23 | } 24 | 25 | function first() { 26 | var_dump(__FUNCTION__ . ' - ' . 1); 27 | yield from delay(1000); 28 | var_dump(__FUNCTION__ . ' - ' . 2); 29 | yield from delay(1000); 30 | yield from second(); 31 | 32 | return 5; 33 | } 34 | 35 | var_dump(await(all(first(), second()))); 36 | 37 | 38 | echo 'Time spent: ' . ($with = microtime(true) - $start) . PHP_EOL; 39 | -------------------------------------------------------------------------------- /examples/ok/wrap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\runtime\{await, all, wrap}; 15 | use function jubianchi\async\time\{delay}; 16 | 17 | $start = microtime(true); 18 | 19 | function two() { 20 | yield 3; 21 | yield 4; 22 | } 23 | 24 | function five() { 25 | yield 1; 26 | yield 2; 27 | yield from two(); 28 | 29 | return 5; 30 | } 31 | 32 | $generators = await( 33 | all( 34 | wrap(five()), 35 | wrap(two()) 36 | ) 37 | ); 38 | 39 | foreach ($generators as $generator) { 40 | foreach ($generator as $v) { 41 | var_dump($v); 42 | } 43 | 44 | var_dump('return: ' . $generator->getReturn()); 45 | } 46 | 47 | echo 'Time spent: ' . ($with = microtime(true) - $start) . PHP_EOL; 48 | -------------------------------------------------------------------------------- /src/socket.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\socket; 15 | 16 | require_once __DIR__ . '/runtime.php'; 17 | 18 | use jubianchi\async\runtime; 19 | 20 | function write($socket, $data, $length = null) : \generator 21 | { 22 | if (is_resource($socket) === false) { 23 | throw new \invalidArgumentException(); 24 | } 25 | 26 | $cancel = false; 27 | $written = 0; 28 | 29 | while ($written < strlen($data) && $cancel == false) { 30 | $write = [$socket]; 31 | $read = $except = null; 32 | 33 | $select = socket_select($read, $write, $except, 0); 34 | 35 | if ($select > 0) { 36 | $written += socket_write($write[0], substr($data, $written), $length ?: -1); 37 | } 38 | 39 | $cancel = yield; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2016 Julien Bianchi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/ok/socket.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\loop\{endless}; 15 | use function jubianchi\async\pipe\{make}; 16 | use function jubianchi\async\runtime\{await, all}; 17 | use function jubianchi\async\socket\{write}; 18 | use function jubianchi\async\stream\{tail}; 19 | 20 | $pipe = make(); 21 | $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 22 | 23 | socket_connect($socket, 0, $argv[1]); 24 | socket_set_nonblock($socket); 25 | 26 | await( 27 | all( 28 | tail(fopen(__DIR__ . '/../data/first.log', 'r'), $pipe), 29 | tail(fopen(__DIR__ . '/../data/second.log', 'r'), $pipe), 30 | endless(function() use ($socket, $pipe) { 31 | $data = yield from $pipe->dequeue(); 32 | 33 | yield from write($socket, $data); 34 | }) 35 | ) 36 | ); 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jubianchi/async-generator", 3 | "description": "An async generator runtime for PHP", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "async", 8 | "generator", 9 | "runtime" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "jubianchi", 14 | "email": "contact@jubianchi.fr" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.0.3" 19 | }, 20 | "require-dev": { 21 | "atoum/atoum": "@stable", 22 | "atoum/reports-extension": "@stable", 23 | "fabpot/php-cs-fixer": "@stable", 24 | "couscous/couscous": "@stable" 25 | }, 26 | "autoload": { 27 | "files": [ 28 | "src/loop/functions.php", 29 | "src/pipe/functions.php", 30 | "src/runtime/functions.php", 31 | "src/time/functions.php", 32 | 33 | "src/stream.php", 34 | "src/socket.php", 35 | "src/process.php" 36 | ] 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "jubianchi\\async\\runtime\\tests\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "cs": "php-cs-fixer fix --ansi", 45 | "test": "atoum -ft", 46 | "couscous": "couscous preview" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/runtime/functions.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\runtime; 15 | 16 | require_once __DIR__ . '/../runtime.php'; 17 | require_once __DIR__ . '/../wrapper.php'; 18 | 19 | use jubianchi\async\runtime; 20 | use jubianchi\async\wrapper; 21 | 22 | function await($generatorOrValue) 23 | { 24 | return runtime::await($generatorOrValue); 25 | } 26 | 27 | function all(...$generators) : \generator 28 | { 29 | return runtime::all(...$generators); 30 | } 31 | 32 | function race($first, $second, ...$generators) : \generator 33 | { 34 | return runtime::race($first, $second, ...$generators); 35 | } 36 | 37 | function some(int $howMany, ...$generators) : \generator 38 | { 39 | return runtime::some($howMany, ...$generators); 40 | } 41 | 42 | function fork(&$generators) : \generator 43 | { 44 | return runtime::fork($generators); 45 | } 46 | 47 | function wrap($value) : wrapper 48 | { 49 | return wrapper::wrap($value); 50 | } 51 | 52 | function unwrap(wrapper $wrapper) 53 | { 54 | return wrapper::unwrap($wrapper); 55 | } 56 | -------------------------------------------------------------------------------- /src/loop/functions.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\loop; 15 | 16 | require_once __DIR__ . '/../loop.php'; 17 | 18 | use jubianchi\async\loop; 19 | 20 | /** 21 | * @api 22 | * 23 | * @param callable|mixed $condition 24 | * @param callable $resolve 25 | * 26 | * @return \generator 27 | */ 28 | function whilst($condition, callable $resolve) : \generator 29 | { 30 | return loop::whilst($condition, $resolve); 31 | } 32 | 33 | /** 34 | * @api 35 | * 36 | * @param callable|mixed $condition 37 | * @param callable $resolve 38 | * 39 | * @return \generator 40 | */ 41 | function until($condition, callable $resolve) : \generator 42 | { 43 | return loop::until($condition, $resolve); 44 | } 45 | 46 | /** 47 | * @api 48 | * 49 | * @param int $time 50 | * @param callable $resolve 51 | * 52 | * @return \generator 53 | */ 54 | function times(int $times, callable $resolve) : \generator 55 | { 56 | return loop::times($times, $resolve); 57 | } 58 | 59 | /** 60 | * @api 61 | * 62 | * @param callable $resolve 63 | * 64 | * @return \generator 65 | */ 66 | function endless(callable $resolve) : \generator 67 | { 68 | return loop::endless($resolve); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-generator 2 | 3 | **This project is in early alpha and is more a like a POC than a real production-ready library.** 4 | 5 | ## Requirements 6 | 7 | The [async-generator](https://github.com/jubianchi/async-generator) library requires PHP `^7.0.3`(i.e PHP `>= 7.0.3 && < 8.0.0`) 8 | 9 | ## Install 10 | 11 | Use [Composer](https://getcomposer.org/) to install this library into your project: 12 | 13 | ```json 14 | { 15 | "require": { 16 | "jubianchi/async-generator": "@stable" 17 | } 18 | } 19 | ``` 20 | 21 | Then run `composer up jubianchi/async-generator` and everything should be ready. 22 | 23 | If you don't want to manually edit your `composer.json` file, simply run `composer require jubianchi/async-generator` 24 | and you should be ready. 25 | 26 | ## Testing 27 | 28 | Once everything is installed, create a simple PHP file: 29 | 30 | ```php 31 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async; 15 | 16 | require_once __DIR__ . '/runtime.php'; 17 | 18 | class loop 19 | { 20 | public static function whilst($condition, callable $resolve) : \generator 21 | { 22 | $cancel = false; 23 | 24 | if (is_callable($condition) === false) { 25 | $condition = function () use ($condition) { return $condition; }; 26 | } 27 | 28 | while ($cancel == false && $condition()) { 29 | $resolved = $resolve(); 30 | 31 | if ($resolved instanceof \generator) { 32 | yield from $resolved; 33 | } 34 | 35 | $cancel = yield; 36 | } 37 | } 38 | 39 | public static function until($condition, callable $resolve) : \generator 40 | { 41 | $condition = function () use ($condition) { 42 | return !(is_callable($condition) ? $condition() : $condition); 43 | }; 44 | 45 | return self::whilst($condition, $resolve); 46 | } 47 | 48 | public static function times(int $times, callable $resolve) : \generator 49 | { 50 | $condition = function () use (&$times) { 51 | return $times-- > 0; 52 | }; 53 | 54 | return self::whilst($condition, $resolve); 55 | } 56 | 57 | public static function endless($resolve) : \generator 58 | { 59 | yield from self::whilst(true, $resolve); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/ok/read.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\runtime\{await, all}; 15 | use function jubianchi\async\stream\{read}; 16 | use function jubianchi\async\time\{delay}; 17 | 18 | const BUFFER_LENGTH = 2048; 19 | 20 | $start = microtime(true); 21 | 22 | $f1 = fopen(__DIR__ . '/../data/d1.dat', 'r'); 23 | $f2 = fopen(__DIR__ . '/../data/d2.dat', 'r'); 24 | 25 | $length = 0; 26 | 27 | while ($buffer = fread($f1, BUFFER_LENGTH)) { 28 | $length += strlen($buffer); 29 | } 30 | 31 | while ($buffer = fread($f2, BUFFER_LENGTH)) { 32 | $length += strlen($buffer); 33 | } 34 | 35 | fclose($f1); 36 | fclose($f2); 37 | 38 | echo 'Found ' . $length . ' characters' . PHP_EOL; 39 | echo 'Time spent without await: ' . ($without = microtime(true) - $start) . PHP_EOL; 40 | 41 | $start = microtime(true); 42 | 43 | $f1 = fopen(__DIR__ . '/../data/d1.dat', 'r'); 44 | $f2 = fopen(__DIR__ . '/../data/d2.dat', 'r'); 45 | 46 | $length = 0; 47 | 48 | await( 49 | all( 50 | read($f1, function($data) use (& $length) { $length += strlen($data); }, BUFFER_LENGTH), 51 | read($f2, function($data) use (& $length) { $length += strlen($data); }, BUFFER_LENGTH) 52 | ) 53 | ); 54 | 55 | echo 'Found ' . $length . ' characters' . PHP_EOL; 56 | echo 'Time spent with await: ' . ($with = microtime(true) - $start) . PHP_EOL; 57 | 58 | if ($without > $with) { 59 | echo 'await is ' . round(($without / $with), 2) . ' times faster' . PHP_EOL; 60 | } else { 61 | echo 'await is ' . round(($with / $without), 2) . ' times slower' . PHP_EOL; 62 | } 63 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Introduction 4 | 5 | PHP is a nice langage: it has been designed to be easy and fast but lacks concurrent programming capabilities. It is a 6 | **sequential** language. 7 | 8 | Developers sometimes want to achieve **concurrent programming** with PHP and often end up using external technologies 9 | like workers. Those workers have many benefits but sometime they are too heavy to just handle simple tasks. 10 | 11 | ### Concurrent programming 12 | 13 | When talking about concurrent programming it's useful to distinguish two methods: 14 | 15 | * Parallel computing 16 | * Concurrent computing 17 | 18 | To sum-up the difference between those two paradigm, let's look at some diagram. 19 | 20 | **Parellel computing** means two tasks will run in parallel without blocking each other: 21 | 22 | ``` 23 | +- - - - - - - - - - -+ 24 | | | 25 | - - + + - - > 26 | | | 27 | +- - - - - - - - - - -+ 28 | ``` 29 | 30 | **Concurrent programming** is a bit different: two tasks will race together and each time a task is paused, the main 31 | program will switch context and work on the other task: 32 | 33 | ``` 34 | +- - - - - -+ 35 | | | 36 | - - + + - - > 37 | | | 38 | +- - - - - - -+ 39 | ``` 40 | 41 | ### Async generators 42 | 43 | Async generators try to solve this issue by **allowing developers to implement concurrent programming**. Thanks to the 44 | `yield` keyword, we can make PHP pause processing something and switch to another task. 45 | 46 | The library provides some low-level functions grouped in logical namespaces to make things easier: 47 | 48 | * [Runtime](runtime.md): this namespace provides generic functions to work with concurrency 49 | * [Time](time.md): this namespace provides functions to work with time 50 | * [Pipe](pipe.md): this namespace provides functions to work with pipes 51 | * [Loop](loop.md): this namespace provides functions to make concurrent loops 52 | * [Stream](stream.md): this namespace provides functions to work with streams 53 | * [Socket](socket.md): this namespace provides functions to work with sockets 54 | 55 | -------------------------------------------------------------------------------- /examples/not-ok/http.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | require_once __DIR__ . '/../../vendor/autoload.php'; 13 | 14 | use function jubianchi\async\runtime\{await, all, fork, race}; 15 | use function jubianchi\async\time\{delay}; 16 | use jubianchi\async\socket; 17 | 18 | $address = '0.0.0.0'; 19 | $port = $_SERVER['argv'][1] ?? 1337; 20 | $queue = []; 21 | 22 | $start = function($socket, $address, $port) use (& $queue) 23 | { 24 | socket_bind($socket, $address, $port); 25 | socket_listen($socket, 0); 26 | socket_set_nonblock($socket); 27 | $index = 0; 28 | $cancel = false; 29 | 30 | while ($cancel == false) { 31 | $client = socket_accept($socket); 32 | 33 | if ($client) { 34 | echo '> Got client...' . PHP_EOL; 35 | 36 | $queue[] = (function() use ($index, $client, $address, $port) { 37 | echo '> Handling request #' . $index . '...' . PHP_EOL; 38 | 39 | $response = 'Hello World!'; 40 | $output = 'HTTP/1.1 200 OK' . "\r\n" . 41 | 'Date: ' . date("D, j M Y G:i:s T") . "\r\n" . 42 | 'Server: AsyncGenerator/1.0.0 (PHP ' . phpversion() . ')' . "\r\n" . 43 | 'Content-Length: ' . strlen($response) . "\r\n" . 44 | 'Content-Type: text/plain' . "\r\n" . 45 | "\r\n" . 46 | $response . "\r\n"; 47 | 48 | yield from delay(1000); 49 | yield from socket\write($client, $output, 5); 50 | 51 | socket_close($client); 52 | })(); 53 | 54 | echo '> Client request #' . $index++ . ' queued...' . PHP_EOL; 55 | } 56 | 57 | $cancel = yield; 58 | } 59 | 60 | socket_close($socket); 61 | }; 62 | 63 | await( 64 | race( 65 | $start(socket_create(AF_INET, SOCK_STREAM, 0), $address, $port), 66 | fork($queue) 67 | //delay(20000) 68 | ) 69 | ); 70 | -------------------------------------------------------------------------------- /src/time.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async; 15 | 16 | require_once __DIR__ . '/runtime.php'; 17 | 18 | class time 19 | { 20 | /** 21 | * @param int $timeout 22 | * @param mixed $resolve 23 | * 24 | * @return \generator 25 | */ 26 | public static function delay(int $timeout, $resolve = null) : \generator 27 | { 28 | $timeout = self::ms2s($timeout); 29 | 30 | yield from self::wait(microtime(true), $timeout); 31 | 32 | return runtime::await($resolve); 33 | } 34 | 35 | /** 36 | * @param int $interval 37 | * @param mixed $resolve 38 | * 39 | * @return \generator 40 | */ 41 | public static function throttle(int $interval, $resolve) : \generator 42 | { 43 | $interval = self::ms2s($interval); 44 | $cancel = false; 45 | 46 | while ($cancel == false) { 47 | yield from self::wait(microtime(true), $interval); 48 | 49 | if ($cancel === true) { 50 | break; 51 | } 52 | 53 | if (is_callable($resolve)) { 54 | $resolved = $resolve(); 55 | } else { 56 | $resolved = $resolve; 57 | } 58 | 59 | if ($resolved instanceof \generator) { 60 | yield from $resolved; 61 | } 62 | 63 | //runtime::await($resolve); 64 | 65 | $cancel = yield; 66 | } 67 | } 68 | 69 | /** 70 | * @param float $time 71 | * 72 | * @return float 73 | */ 74 | private static function ms2s(int $time) : float 75 | { 76 | return $time / 1000; 77 | } 78 | 79 | private static function wait(float $from, float $for) : \generator 80 | { 81 | $cancel = false; 82 | 83 | while ((microtime(true) - $from) < $for && $cancel == false) { 84 | usleep(1); 85 | 86 | $cancel = yield; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/stream.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\stream; 15 | 16 | require_once __DIR__ . '/runtime.php'; 17 | 18 | use jubianchi\async\runtime; 19 | 20 | function tail($stream, callable $data = null, $length = null) : \generator 21 | { 22 | if (is_resource($stream) === false) { 23 | throw new \invalidArgumentException(); 24 | } 25 | 26 | $data = $data ?? function () {}; 27 | $cancel = false; 28 | 29 | while ($cancel == false) { 30 | $read = [$stream]; 31 | $write = $except = null; 32 | 33 | $select = stream_select($read, $write, $except, 0); 34 | 35 | if ($select > 0) { 36 | while ($buffer = stream_get_contents($read[0], $length ?: -1)) { 37 | $data($buffer); 38 | } 39 | } 40 | 41 | $cancel = yield; 42 | } 43 | } 44 | 45 | function read($stream, callable $data = null, $length = null) : \generator 46 | { 47 | if (is_resource($stream) === false) { 48 | throw new \invalidArgumentException(); 49 | } 50 | 51 | $data = $data ?? function () {}; 52 | 53 | do { 54 | $read = [$stream]; 55 | $write = $except = null; 56 | 57 | $select = stream_select($read, $write, $except, 0); 58 | 59 | if ($select > 0) { 60 | $buffer = stream_get_contents($read[0], $length ?: -1); 61 | 62 | if ($buffer !== '') { 63 | runtime::await($data($buffer)); 64 | } 65 | } 66 | 67 | $cancel = yield; 68 | 69 | $metadata = stream_get_meta_data($stream); 70 | } while (($metadata['eof'] === false || $metadata['unread_bytes'] > 0) && $cancel == false); 71 | } 72 | 73 | function write($stream, $data, $length = null) : \generator 74 | { 75 | if (is_resource($stream) === false) { 76 | throw new \invalidArgumentException(); 77 | } 78 | 79 | $cancel = false; 80 | $written = 0; 81 | 82 | while ($written < strlen($data) && $cancel == false) { 83 | $write = [$stream]; 84 | $read = $except = null; 85 | 86 | $select = stream_select($read, $write, $exce, 0); 87 | 88 | if ($select > 0) { 89 | $written += fwrite($write[0], substr($data, $written), $length ?: -1); 90 | } 91 | 92 | $cancel = yield; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | 7 | 8 | This source file is subject to the MIT license that is bundled 9 | with this source code in the file LICENSE. 10 | EOF; 11 | 12 | Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header); 13 | 14 | return Symfony\CS\Config\Config::create() 15 | // use default SYMFONY_LEVEL and extra fixers: 16 | ->fixers(array( 17 | 'psr0', 18 | 'encoding', 19 | 'braces', 20 | 'elseif', 21 | 'eof_ending', 22 | 'function_call_space', 23 | 'function_declaration', 24 | 'indentation', 25 | 'line_after_namespace', 26 | 'linefeed', 27 | 'lowercase_constants', 28 | 'lowercase_keywords', 29 | 'method_argument_space', 30 | 'multiple_use', 31 | 'parenthesis', 32 | 'php_closing_tag', 33 | 'single_line_after_imports', 34 | 'trailing_spaces', 35 | 'visibility', 36 | 'array_element_no_space_before_comma', 37 | 'array_element_white_space_after_comma', 38 | 'blankline_after_open_tag', 39 | 'duplicate_semicolon', 40 | 'extra_empty_lines', 41 | 'function_typehint_space', 42 | 'namespace_no_leading_whitespace', 43 | 'new_with_braces', 44 | 'no_blank_lines_after_class_opening', 45 | 'no_empty_lines_after_phpdocs', 46 | 'object_operator', 47 | 'operators_spaces', 48 | 'phpdoc_indent', 49 | 'phpdoc_inline_tag', 50 | 'phpdoc_no_access', 51 | 'phpdoc_no_empty_return', 52 | 'phpdoc_params', 53 | 'phpdoc_scalar', 54 | 'phpdoc_separation', 55 | 'phpdoc_short_description', 56 | 'remove_leading_slash_use', 57 | 'remove_lines_between_uses', 58 | 'return', 59 | 'self_accessor', 60 | 'single_array_no_trailing_comma', 61 | 'single_blank_line_before_namespace', 62 | 'single_quote', 63 | 'spaces_cast', 64 | 'ternary_spaces', 65 | 'trim_array_spaces', 66 | 'unary_operators_spaces', 67 | 'unused_use', 68 | 'whitespacy_lines', 69 | 'concat_with_spaces', 70 | 'header_comment', 71 | 'ordered_use', 72 | 'phpdoc_order', 73 | 'short_array_syntax' 74 | )) 75 | ->finder( 76 | Symfony\CS\Finder\DefaultFinder::create() 77 | ->in(__DIR__ . DIRECTORY_SEPARATOR . 'src') 78 | ->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests') 79 | ->in(__DIR__ . DIRECTORY_SEPARATOR . 'examples') 80 | ) 81 | ; 82 | -------------------------------------------------------------------------------- /tests/units/src/runtime/functions/await.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units\runtime; 13 | 14 | use function jubianchi\async\runtime\await; 15 | use jubianchi\async\runtime; 16 | use jubianchi\async\runtime\tests\func; 17 | 18 | class await extends func 19 | { 20 | /** @dataProvider valueDataProvider */ 21 | public function testAwaitValue($value) 22 | { 23 | $this->variable(await($value))->isIdenticalTo($value); 24 | } 25 | 26 | /** @dataProvider valueDataProvider */ 27 | public function testAwaitGenerator($value) 28 | { 29 | $this 30 | ->given($creator = function ($limit, $value) { 31 | while ($limit-- > 0) { 32 | yield; 33 | } 34 | 35 | return $value; 36 | }) 37 | ->then 38 | ->variable(await($creator(3, $value)))->isIdenticalTo($value) 39 | ; 40 | } 41 | 42 | /** @dataProvider valueDataProvider */ 43 | public function testAwaitGeneratorCreator($value) 44 | { 45 | $this 46 | ->given($creator = function ($limit, $value) { 47 | while ($limit-- > 0) { 48 | yield; 49 | } 50 | 51 | return (function () use ($value) { 52 | yield; 53 | 54 | return $value; 55 | })(); 56 | }) 57 | ->then 58 | ->variable(await($creator(3, $value)))->isIdenticalTo($value) 59 | ; 60 | } 61 | 62 | /** @dataProvider valueDataProvider */ 63 | public function testAwaitWrappedGeneratorCreator($value) 64 | { 65 | $this 66 | ->given($creator = function ($limit, $value) { 67 | while ($limit-- > 0) { 68 | yield; 69 | } 70 | 71 | return (function () use ($value) { 72 | yield; 73 | 74 | return $value; 75 | })(); 76 | }) 77 | ->then 78 | ->object(await(runtime\wrap($creator(3, $value))))->isInstanceOf(\generator::class) 79 | ; 80 | } 81 | 82 | protected function valueDataProvider() 83 | { 84 | return [ 85 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 86 | [1 / 3, 7 / 5], 87 | [uniqid(), uniqid()], 88 | [false, true], 89 | [true, false], 90 | [null], 91 | [range(0, 2), range(2, 4)], 92 | [new \stdClass(), new \stdClass()], 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/process.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async\process; 15 | 16 | function spawn(string $cmd, callable $output = null, callable $error = null) : \generator 17 | { 18 | $output = $output ?? function () {}; 19 | $error = $error ?? function () {}; 20 | 21 | $pipes = []; 22 | $proc = proc_open( 23 | $cmd, 24 | [ 25 | 0 => ['pipe', 'r'], 26 | 1 => ['pipe', 'w'], 27 | 2 => ['pipe', 'w'], 28 | ], 29 | $pipes 30 | ); 31 | $cancel = false; 32 | 33 | while ($cancel == false && proc_get_status($proc)['running'] === true) { 34 | $read = ['output' => $pipes[1], 'error' => $pipes[2]]; 35 | $write = $except = null; 36 | 37 | if (stream_select($read, $write, $except, 0, 1) > 0) { 38 | foreach ($read as $key => $stream) { 39 | $buffer = stream_get_contents($stream); 40 | 41 | if ($buffer !== '') { 42 | ${$key}($buffer); 43 | } 44 | } 45 | } 46 | 47 | $cancel = yield; 48 | } 49 | 50 | if ($cancel === true) { 51 | proc_terminate($proc, SIGKILL); 52 | } else { 53 | $read = ['output' => $pipes[1], 'error' => $pipes[2]]; 54 | $write = $except = null; 55 | 56 | if (stream_select($read, $write, $except, 0, 1) > 0) { 57 | foreach ($read as $key => $stream) { 58 | $buffer = stream_get_contents($stream); 59 | 60 | if ($buffer !== '') { 61 | ${$key}($buffer); 62 | } 63 | } 64 | } 65 | } 66 | 67 | $status = proc_get_status($proc); 68 | 69 | proc_close($proc); 70 | 71 | return $status; 72 | } 73 | 74 | function passthru(string $cmd) : \generator 75 | { 76 | $pipes = []; 77 | $proc = proc_open( 78 | $cmd, 79 | [ 80 | 0 => ['pipe', 'r'], 81 | 1 => ['pipe', 'w'], 82 | 2 => ['pipe', 'w'], 83 | ], 84 | $pipes 85 | ); 86 | $cancel = false; 87 | $contents = ''; 88 | 89 | while ($cancel == false && proc_get_status($proc)['running'] === true) { 90 | $out = [$pipes[1]]; 91 | $err = [$pipes[2]]; 92 | $write = $except = null; 93 | 94 | if (stream_select($out, $write, $except, 0, 1) > 0) { 95 | $buffer = stream_get_contents($out[0]); 96 | 97 | if ($buffer !== '') { 98 | $contents .= $buffer; 99 | } 100 | } 101 | 102 | if (stream_select($err, $write, $except, 0, 1) > 0) { 103 | $buffer = stream_get_contents($err[0]); 104 | 105 | if ($buffer !== '') { 106 | $contents .= $buffer; 107 | } 108 | } 109 | 110 | $cancel = yield; 111 | } 112 | 113 | if ($cancel === true) { 114 | proc_terminate($proc); 115 | } else { 116 | $out = [$pipes[1]]; 117 | $err = [$pipes[2]]; 118 | $write = $except = null; 119 | 120 | if (stream_select($out, $write, $except, 0, 1) > 0) { 121 | $buffer = stream_get_contents($out[0]); 122 | 123 | if ($buffer !== '') { 124 | $contents .= $buffer; 125 | } 126 | } 127 | 128 | if (stream_select($err, $write, $except, 0, 1) > 0) { 129 | $buffer = stream_get_contents($err[0]); 130 | 131 | if ($buffer !== '') { 132 | $contents .= $buffer; 133 | } 134 | } 135 | } 136 | 137 | proc_close($proc); 138 | 139 | return $contents; 140 | } 141 | -------------------------------------------------------------------------------- /tests/units/src/runtime/functions/race.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units\runtime; 13 | 14 | use function jubianchi\async\runtime\race; 15 | use jubianchi\async\runtime; 16 | use jubianchi\async\runtime\tests\func; 17 | 18 | class race extends func 19 | { 20 | /** @dataProvider valueDataProvider */ 21 | public function testRaceValues($value, $otherValue = null) 22 | { 23 | $this 24 | ->given($otherValue = $otherValue ?? $value) 25 | ->then 26 | ->object($generator = race($value, $otherValue))->isInstanceOf(\generator::class) 27 | ->variable(runtime\await($generator))->isIdenticalTo($value) 28 | ; 29 | } 30 | 31 | /** @dataProvider valueDataProvider */ 32 | public function testRaceGenerators($value, $otherValue = null) 33 | { 34 | $this 35 | ->given( 36 | $otherValue = $otherValue ?? $value, 37 | $creator = function ($limit, $value) { 38 | while ($limit-- > 0) { 39 | yield; 40 | } 41 | 42 | return $value; 43 | } 44 | ) 45 | ->then 46 | ->object($generator = race($creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 47 | ->variable(runtime\await($generator))->isIdenticalTo($otherValue) 48 | ; 49 | } 50 | 51 | /** @dataProvider valueDataProvider */ 52 | public function testRaceGeneratorCreators($value, $otherValue = null) 53 | { 54 | $this 55 | ->given( 56 | $otherValue = $otherValue ?? $value, 57 | $creator = function ($limit, $value) { 58 | while ($limit-- > 0) { 59 | yield; 60 | } 61 | 62 | return (function () use ($value) { 63 | yield; 64 | 65 | return $value; 66 | })(); 67 | } 68 | ) 69 | ->then 70 | ->object($generator = race($creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 71 | ->variable(runtime\await($generator))->isIdenticalTo($otherValue) 72 | ; 73 | } 74 | 75 | /** @dataProvider valueDataProvider */ 76 | public function testRaceWrappedGeneratorCreators($value, $otherValue = null) 77 | { 78 | $this 79 | ->given( 80 | $otherValue = $otherValue ?? $value, 81 | $creator = function ($limit, $value) { 82 | while ($limit-- > 0) { 83 | yield; 84 | } 85 | 86 | return (function () use ($value) { 87 | yield; 88 | 89 | return $value; 90 | })(); 91 | } 92 | ) 93 | ->then 94 | ->object($generator = race(runtime\wrap($creator(5, $value)), runtime\wrap($creator(3, $otherValue))))->isInstanceOf(\generator::class) 95 | ->object(runtime\await($generator))->isInstanceOf(\generator::class) 96 | ->variable(runtime\await(runtime\await($generator)))->isIdenticalTo($value) 97 | ; 98 | } 99 | 100 | protected function valueDataProvider() 101 | { 102 | return [ 103 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 104 | [1 / 3, 7 / 5], 105 | [uniqid(), uniqid()], 106 | [false, true], 107 | [true, false], 108 | [null], 109 | [range(0, 2), range(2, 4)], 110 | [new \stdClass(), new \stdClass()], 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/units/src/time/functions/delay.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units\time; 13 | 14 | use function jubianchi\async\time\delay; 15 | use jubianchi\async\runtime; 16 | use jubianchi\async\runtime\tests\func; 17 | 18 | class delay extends func 19 | { 20 | /** @dataProvider valueDataProvider */ 21 | public function testDelayValue($value) 22 | { 23 | static $index = 0; 24 | 25 | $this 26 | ->given( 27 | $timeout = 500, 28 | $current = 0, 29 | $this->function->microtime = function () use ($timeout, &$current) { 30 | $value = $current; 31 | 32 | $current += ($timeout / 1000); 33 | 34 | return $value; 35 | } 36 | ) 37 | ->then 38 | ->object($generator = delay($timeout, $value))->isInstanceOf(\generator::class) 39 | ->variable(runtime\await($generator))->isIdenticalTo($value) 40 | ->function('microtime') 41 | ->wasCalledWithArguments(true)->exactly(++$index * 2) 42 | ; 43 | } 44 | 45 | /** @dataProvider valueDataProvider */ 46 | public function testDelayGenerator($value) 47 | { 48 | static $index = 0; 49 | 50 | $this 51 | ->given( 52 | $timeout = 500, 53 | $current = 0, 54 | $this->function->microtime = function () use ($timeout, &$current) { 55 | $value = $current; 56 | 57 | $current += ($timeout / 1000); 58 | 59 | return $value; 60 | }, 61 | $creator = function ($limit, $value) { 62 | while ($limit-- > 0) { 63 | yield; 64 | } 65 | 66 | return (function () use ($value) { 67 | yield; 68 | 69 | return $value; 70 | })(); 71 | } 72 | ) 73 | ->then 74 | ->object($generator = delay($timeout, $creator(3, $value)))->isInstanceOf(\generator::class) 75 | ->variable(runtime\await($generator))->isIdenticalTo($value) 76 | ->function('microtime') 77 | ->wasCalledWithArguments(true)->exactly(++$index * 2) 78 | ; 79 | } 80 | 81 | /** @dataProvider valueDataProvider */ 82 | public function testDelayGeneratorCreator($value) 83 | { 84 | static $index = 0; 85 | 86 | $this 87 | ->given( 88 | $timeout = 500, 89 | $current = 0, 90 | $this->function->microtime = function () use ($timeout, &$current) { 91 | $value = $current; 92 | 93 | $current += ($timeout / 1000); 94 | 95 | return $value; 96 | }, 97 | $creator = function ($limit, $value) { 98 | while ($limit-- > 0) { 99 | yield; 100 | } 101 | 102 | return (function () use ($value) { 103 | yield; 104 | 105 | return $value; 106 | })(); 107 | } 108 | ) 109 | ->then 110 | ->object($generator = delay($timeout, $creator(3, $value)))->isInstanceOf(\generator::class) 111 | ->variable(runtime\await($generator))->isIdenticalTo($value) 112 | ->function('microtime') 113 | ->wasCalledWithArguments(true)->exactly(++$index * 2) 114 | ; 115 | } 116 | 117 | protected function valueDataProvider() 118 | { 119 | return [ 120 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 121 | [1 / 3, 7 / 5], 122 | [uniqid(), uniqid()], 123 | [false, true], 124 | [true, false], 125 | [null], 126 | [range(0, 2), range(2, 4)], 127 | [new \stdClass(), new \stdClass()], 128 | ]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/units/src/time.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units; 13 | 14 | use jubianchi\async; 15 | use jubianchi\async\time as testedClass; 16 | use mageekguy\atoum; 17 | 18 | class time extends atoum\test 19 | { 20 | /** @dataProvider valueDataProvider */ 21 | public function testDelayValue($value) 22 | { 23 | static $index = 0; 24 | 25 | $this 26 | ->given( 27 | $timeout = 500, 28 | $current = 0, 29 | $this->function->microtime = function () use ($timeout, &$current) { 30 | $value = $current; 31 | 32 | $current += ($timeout / 1000); 33 | 34 | return $value; 35 | } 36 | ) 37 | ->then 38 | ->object($generator = testedClass::delay($timeout, $value))->isInstanceOf(\generator::class) 39 | ->variable(async\runtime::await($generator))->isIdenticalTo($value) 40 | ->function('microtime') 41 | ->wasCalledWithArguments(true)->exactly(++$index * 2) 42 | ; 43 | } 44 | 45 | /** @dataProvider valueDataProvider */ 46 | public function testDelayGenerator($value) 47 | { 48 | static $index = 0; 49 | 50 | $this 51 | ->given( 52 | $timeout = 500, 53 | $current = 0, 54 | $this->function->microtime = function () use ($timeout, &$current) { 55 | $value = $current; 56 | 57 | $current += ($timeout / 1000); 58 | 59 | return $value; 60 | }, 61 | $creator = function ($limit, $value) { 62 | while ($limit-- > 0) { 63 | yield; 64 | } 65 | 66 | return (function () use ($value) { 67 | yield; 68 | 69 | return $value; 70 | })(); 71 | } 72 | ) 73 | ->then 74 | ->object($generator = testedClass::delay($timeout, $creator(3, $value)))->isInstanceOf(\generator::class) 75 | ->variable(async\runtime::await($generator))->isIdenticalTo($value) 76 | ->function('microtime') 77 | ->wasCalledWithArguments(true)->exactly(++$index * 2) 78 | ; 79 | } 80 | 81 | /** @dataProvider valueDataProvider */ 82 | public function testDelayGeneratorCreator($value) 83 | { 84 | static $index = 0; 85 | 86 | $this 87 | ->given( 88 | $timeout = 500, 89 | $current = 0, 90 | $this->function->microtime = function () use ($timeout, &$current) { 91 | $value = $current; 92 | 93 | $current += ($timeout / 1000); 94 | 95 | return $value; 96 | }, 97 | $creator = function ($limit, $value) { 98 | while ($limit-- > 0) { 99 | yield; 100 | } 101 | 102 | return (function () use ($value) { 103 | yield; 104 | 105 | return $value; 106 | })(); 107 | } 108 | ) 109 | ->then 110 | ->object($generator = testedClass::delay($timeout, $creator(3, $value)))->isInstanceOf(\generator::class) 111 | ->variable(async\runtime::await($generator))->isIdenticalTo($value) 112 | ->function('microtime') 113 | ->wasCalledWithArguments(true)->exactly(++$index * 2) 114 | ; 115 | } 116 | 117 | protected function valueDataProvider() 118 | { 119 | return [ 120 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 121 | [1 / 3, 7 / 5], 122 | [uniqid(), uniqid()], 123 | [false, true], 124 | [true, false], 125 | [null], 126 | [range(0, 2), range(2, 4)], 127 | [new \stdClass(), new \stdClass()], 128 | ]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/units/src/runtime/functions/all.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units\runtime; 13 | 14 | use function jubianchi\async\runtime\all; 15 | use jubianchi\async\runtime; 16 | use jubianchi\async\runtime\tests\func; 17 | 18 | class all extends func 19 | { 20 | /** @dataProvider valueDataProvider */ 21 | public function testAllValue($value) 22 | { 23 | $this 24 | ->object($generator = all($value))->isInstanceOf(\generator::class) 25 | ->array(runtime\await($generator))->isIdenticalTo([$value]) 26 | ; 27 | } 28 | 29 | /** @dataProvider valueDataProvider */ 30 | public function testAllValues($value, $otherValue = null) 31 | { 32 | $this 33 | ->given($otherValue = $otherValue ?? $value) 34 | ->then 35 | ->object($generator = all($value, $otherValue))->isInstanceOf(\generator::class) 36 | ->array(runtime\await($generator))->isIdenticalTo([$value, $otherValue]) 37 | ; 38 | } 39 | 40 | /** @dataProvider valueDataProvider */ 41 | public function testAllGenerator($value) 42 | { 43 | $this 44 | ->given($creator = function ($limit, $value) { 45 | while ($limit-- > 0) { 46 | yield; 47 | } 48 | 49 | return $value; 50 | }) 51 | ->then 52 | ->object($generator = all($creator(3, $value)))->isInstanceOf(\generator::class) 53 | ->array(runtime\await($generator))->isIdenticalTo([$value]) 54 | ; 55 | } 56 | 57 | /** @dataProvider valueDataProvider */ 58 | public function testAllGenerators($value, $otherValue = null) 59 | { 60 | $this 61 | ->given( 62 | $otherValue = $otherValue ?? $value, 63 | $creator = function ($limit, $value) { 64 | while ($limit-- > 0) { 65 | yield; 66 | } 67 | 68 | return $value; 69 | } 70 | ) 71 | ->then 72 | ->object($generator = all($creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 73 | ->array(runtime\await($generator))->isIdenticalTo([$value, $otherValue]) 74 | ; 75 | } 76 | 77 | /** @dataProvider valueDataProvider */ 78 | public function testAllGeneratorCreators($value, $otherValue = null) 79 | { 80 | $this 81 | ->given( 82 | $otherValue = $otherValue ?? $value, 83 | $creator = function ($limit, $value) { 84 | while ($limit-- > 0) { 85 | yield; 86 | } 87 | 88 | return (function () use ($value) { 89 | yield; 90 | 91 | return $value; 92 | })(); 93 | } 94 | ) 95 | ->then 96 | ->object($generator = all($creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 97 | ->array(runtime\await($generator))->isIdenticalTo([$value, $otherValue]) 98 | ; 99 | } 100 | 101 | /** @dataProvider valueDataProvider */ 102 | public function testAllWrappedGeneratorCreators($value, $otherValue = null) 103 | { 104 | $this 105 | ->given( 106 | $otherValue = $otherValue ?? $value, 107 | $creator = function ($limit, $value) { 108 | while ($limit-- > 0) { 109 | yield; 110 | } 111 | 112 | return (function () use ($value) { 113 | yield; 114 | 115 | return $value; 116 | })(); 117 | } 118 | ) 119 | ->then 120 | ->object($generator = all(runtime\wrap($creator(3, $value)), runtime\wrap($creator(5, $otherValue))))->isInstanceOf(\generator::class) 121 | ->array(runtime\await($generator)) 122 | ->object[0]->isInstanceOf(\generator::class) 123 | ->object[1]->isInstanceOf(\generator::class) 124 | ; 125 | } 126 | 127 | protected function valueDataProvider() 128 | { 129 | return [ 130 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 131 | [1 / 3, 7 / 5], 132 | [uniqid(), uniqid()], 133 | [false, true], 134 | [true, false], 135 | [null], 136 | [range(0, 2), range(2, 4)], 137 | [new \stdClass(), new \stdClass()], 138 | ]; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/units/src/runtime/functions/some.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units\runtime; 13 | 14 | use function jubianchi\async\runtime\some; 15 | use jubianchi\async\runtime; 16 | use jubianchi\async\runtime\tests\func; 17 | 18 | class some extends func 19 | { 20 | /** @dataProvider valueDataProvider */ 21 | public function testSomeValue($value) 22 | { 23 | $this 24 | ->object($generator = some(1, $value))->isInstanceOf(\generator::class) 25 | ->array(runtime\await($generator))->isIdenticalTo([$value]) 26 | ->exception(function () use (&$expected) { 27 | runtime\await(some($expected = rand(2, PHP_INT_MAX), uniqid())); 28 | } 29 | ) 30 | ->isInstanceOf(\logicException::class) 31 | ->hasMessage(sprintf('Some expected at least %d generators', $expected)) 32 | ; 33 | } 34 | 35 | /** @dataProvider valueDataProvider */ 36 | public function testSomeValues($value, $otherValue = null) 37 | { 38 | $this 39 | ->given($otherValue = $otherValue ?? $value) 40 | ->then 41 | ->object($generator = some(1, $value, $otherValue))->isInstanceOf(\generator::class) 42 | ->variable(runtime\await($generator))->isIdenticalTo([$value]) 43 | ->object($generator = some(2, $value, $otherValue))->isInstanceOf(\generator::class) 44 | ->variable(runtime\await($generator))->isIdenticalTo([$value, $otherValue]) 45 | ; 46 | } 47 | 48 | /** @dataProvider valueDataProvider */ 49 | public function testSomeGenerators($value, $otherValue = null) 50 | { 51 | $this 52 | ->given($creator = function ($limit, $value) { 53 | while ($limit-- > 0) { 54 | yield; 55 | } 56 | 57 | return $value; 58 | }) 59 | ->then 60 | ->object($generator = some(1, $creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 61 | ->variable(runtime\await($generator))->isIdenticalTo([$value]) 62 | ->object($generator = some(2, $creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 63 | ->variable(runtime\await($generator))->isIdenticalTo([$value, $otherValue]) 64 | ; 65 | } 66 | 67 | /** @dataProvider valueDataProvider */ 68 | public function testSomeGeneratorCreators($value, $otherValue = null) 69 | { 70 | $this 71 | ->given( 72 | $otherValue = $otherValue ?? $value, 73 | $creator = function ($limit, $value) { 74 | while ($limit-- > 0) { 75 | yield; 76 | } 77 | 78 | return (function () use ($value) { 79 | yield; 80 | 81 | return $value; 82 | })(); 83 | } 84 | ) 85 | ->then 86 | ->object($generator = some(1, $creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 87 | ->array(runtime\await($generator))->isIdenticalTo([1 => $otherValue]) 88 | ->object($generator = some(2, $creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 89 | ->array(runtime\await($generator))->isIdenticalTo([$value, $otherValue]) 90 | ; 91 | } 92 | 93 | /** @dataProvider valueDataProvider */ 94 | public function testSomeWrappedGeneratorCreators($value, $otherValue = null) 95 | { 96 | $this 97 | ->given( 98 | $otherValue = $otherValue ?? $value, 99 | $creator = function ($limit, $value) { 100 | while ($limit-- > 0) { 101 | yield; 102 | } 103 | 104 | return (function () use ($value) { 105 | yield; 106 | 107 | return $value; 108 | })(); 109 | } 110 | ) 111 | ->then 112 | ->object($generator = some(1, runtime\wrap($creator(5, $value)), runtime\wrap($creator(3, $otherValue))))->isInstanceOf(\generator::class) 113 | ->array(runtime\await($generator)) 114 | ->object[0]->isInstanceOf(\generator::class) 115 | ->array(runtime\await(runtime\await($generator)))->isIdenticalTo([$value]) 116 | ->object($generator = some(2, runtime\wrap($creator(5, $value)), runtime\wrap($creator(3, $otherValue))))->isInstanceOf(\generator::class) 117 | ->array(runtime\await($generator)) 118 | ->object[0]->isInstanceOf(\generator::class) 119 | ->object[1]->isInstanceOf(\generator::class) 120 | ->array(runtime\await(runtime\await($generator)))->isIdenticalTo([$value, $otherValue]) 121 | ; 122 | } 123 | 124 | protected function valueDataProvider() 125 | { 126 | return [ 127 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 128 | [1 / 3, 7 / 5], 129 | [uniqid(), uniqid()], 130 | [false, true], 131 | [true, false], 132 | [null], 133 | [range(0, 2), range(2, 4)], 134 | [new \stdClass(), new \stdClass()], 135 | ]; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /docs/runtime.md: -------------------------------------------------------------------------------- 1 | ## Runtime (`jubianchi\async\runtime`) 2 | 3 | ### `jubianchi\async\runtime::await(mixed $generatorOrValue) : mixed` 4 | 5 | `await` takes a generator or a value as its single parameter. 6 | 7 | When it gets a generator it will walk through it until its end. It will return the generator 8 | return value. 9 | 10 | Given the following script where the `producer` function returns a generator: 11 | 12 | ```php 13 | 93 | string(5) "19-13" 94 | [1]=> 95 | string(5) "20-13" 96 | } 97 | ``` 98 | 99 | As you can see, the `all` call will walk through all generators **alternately** and resolve to an array containing all the 100 | generators return values in the same order as the arguments passed to `all`. 101 | 102 | **What's interesting here is that each `yield` is an opportunity for the runtime to switch task.** 103 | 104 | ### `jubianchi\async\runtime::race($first, $second, ...$generators) : \generator` 105 | 106 | `race` takes several generator as its arguments and will walk **concurrently** through each of them. It will return 107 | a generator which will resolve with the value of the first finished generator. 108 | 109 | Given the following script where the `producer` function returns a generator: 110 | 111 | ```php 112 | 220 | string(5) "26-19" 221 | [2] => 222 | string(5) "28-19" 223 | } 224 | ``` 225 | 226 | `some` has a similar behavior than the `race` function. The only difference is that it will wait for a given number of 227 | generators to finish. It will return a generator which will resolve to an array containing all the generators' return 228 | values. 229 | -------------------------------------------------------------------------------- /src/runtime.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | declare (strict_types = 1); 13 | 14 | namespace jubianchi\async; 15 | 16 | require_once __DIR__ . '/wrapper.php'; 17 | 18 | final class runtime 19 | { 20 | /** 21 | * @param mixed $generatorOrValue 22 | * 23 | * @return mixed 24 | */ 25 | public static function await($generatorOrValue) 26 | { 27 | switch (true) { 28 | case $generatorOrValue instanceof \generator: 29 | $generatorOrValue->current(); 30 | 31 | while ($generatorOrValue->valid() === true) { 32 | $generatorOrValue->next(); 33 | } 34 | 35 | return self::await($generatorOrValue->getReturn()); 36 | 37 | case is_callable($generatorOrValue): 38 | return self::await($generatorOrValue()); 39 | 40 | case $generatorOrValue instanceof wrapper: 41 | return wrapper::unwrap($generatorOrValue); 42 | 43 | case is_array($generatorOrValue) === true: 44 | return array_map( 45 | function ($generator) { 46 | return self::await($generator); 47 | }, 48 | $generatorOrValue 49 | ); 50 | 51 | default: 52 | return $generatorOrValue; 53 | } 54 | } 55 | 56 | /** 57 | * @param \generator[] ...$generators 58 | * 59 | * @return \generator 60 | */ 61 | public static function all(...$generators) : \generator 62 | { 63 | return self::some(count($generators), ...$generators); 64 | } 65 | 66 | /** 67 | * @param \generator|mixed $first 68 | * @param \generator|mixed $second 69 | * @param \generator[] ...$generators 70 | * 71 | * @return \generator 72 | */ 73 | public static function race($first, $second, ...$generators) : \generator 74 | { 75 | $cancel = false; 76 | $started = []; 77 | array_unshift($generators, $second); 78 | array_unshift($generators, $first); 79 | 80 | while (count($generators) > 0 && $cancel == false) { 81 | $generator = current($generators); 82 | $key = key($generators); 83 | 84 | switch (true) { 85 | case $generator instanceof \generator: 86 | if (in_array($generator, $started, true)) { 87 | $generator->next(); 88 | } else { 89 | $generator->current(); 90 | $started[] = $generator; 91 | } 92 | 93 | if ($generator->valid() === false) { 94 | unset($generators[$key]); 95 | 96 | return self::await(self::cancel($generator->getReturn(), $generators)); 97 | } 98 | break; 99 | 100 | default: 101 | return $generator; 102 | } 103 | 104 | self::reset($generators); 105 | 106 | yield; 107 | } 108 | } 109 | 110 | /** 111 | * @param int $howMany 112 | * @param \generator[] ...$generators 113 | * 114 | * @return \generator 115 | */ 116 | public static function some(int $howMany, ...$generators) : \generator 117 | { 118 | if ($howMany > count($generators)) { 119 | throw new \logicException(sprintf('Some expected at least %d generators', $howMany)); 120 | } 121 | 122 | $results = []; 123 | $started = []; 124 | 125 | while (count($generators) > 0 && count($results) < $howMany) { 126 | $generator = current($generators); 127 | $key = key($generators); 128 | 129 | switch (true) { 130 | case $generator instanceof \generator: 131 | if (in_array($generator, $started, true)) { 132 | $generator->next(); 133 | } else { 134 | $generator->current(); 135 | $started[] = $generator; 136 | } 137 | 138 | 139 | if ($generator->valid() === false) { 140 | unset($generators[$key]); 141 | 142 | $results[$key] = self::await($generator->getReturn()); 143 | } 144 | break; 145 | 146 | default: 147 | unset($generators[$key]); 148 | 149 | $results[$key] = $generator; 150 | break; 151 | } 152 | 153 | self::reset($generators); 154 | 155 | yield; 156 | } 157 | 158 | ksort($results); 159 | 160 | return self::cancel($results, $generators); 161 | } 162 | 163 | /** 164 | * @param \generator[] $generators 165 | * 166 | * @return \generator 167 | */ 168 | public static function fork(array &$generators) : \generator 169 | { 170 | $cancel = false; 171 | 172 | while ($cancel == false) { 173 | $key = key($generators); 174 | $generator = current($generators); 175 | 176 | if ($generator instanceof \generator) { 177 | $generator->next(); 178 | 179 | if ($generator->valid() === false) { 180 | unset($generators[$key]); 181 | } 182 | } else { 183 | unset($generators[$key]); 184 | } 185 | 186 | self::reset($generators); 187 | 188 | $cancel = yield; 189 | } 190 | } 191 | 192 | private static function cancel($result, array $generators) 193 | { 194 | foreach ($generators as $generator) { 195 | if ($generator instanceof \generator && $generator->valid()) { 196 | $generator->send(true); 197 | } 198 | } 199 | 200 | return $result; 201 | } 202 | 203 | private static function reset(array &$generators) 204 | { 205 | $reset = next($generators); 206 | 207 | if ($reset === false) { 208 | $reset = reset($generators); 209 | } 210 | 211 | return $reset; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /tests/units/src/runtime.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace jubianchi\async\tests\units; 13 | 14 | use jubianchi\async\runtime as testedClass; 15 | use mageekguy\atoum; 16 | 17 | class runtime extends atoum\test 18 | { 19 | /** @dataProvider valueDataProvider */ 20 | public function testAwaitValue($value) 21 | { 22 | $this->variable(testedClass::await($value))->isIdenticalTo($value); 23 | } 24 | 25 | /** @dataProvider valueDataProvider */ 26 | public function testAwaitGenerator($value) 27 | { 28 | $this 29 | ->given($creator = function ($limit, $value) { 30 | while ($limit-- > 0) { 31 | yield; 32 | } 33 | 34 | return $value; 35 | }) 36 | ->then 37 | ->variable(testedClass::await($creator(3, $value)))->isIdenticalTo($value) 38 | ; 39 | } 40 | 41 | /** @dataProvider valueDataProvider */ 42 | public function testAwaitGeneratorCreator($value) 43 | { 44 | $this 45 | ->given($creator = function ($limit, $value) { 46 | while ($limit-- > 0) { 47 | yield; 48 | } 49 | 50 | return (function () use ($value) { 51 | yield; 52 | 53 | return $value; 54 | })(); 55 | }) 56 | ->then 57 | ->variable(testedClass::await($creator(3, $value)))->isIdenticalTo($value) 58 | ; 59 | } 60 | 61 | /** @dataProvider valueDataProvider */ 62 | public function testAllValue($value) 63 | { 64 | $this 65 | ->object($generator = testedClass::all($value))->isInstanceOf(\generator::class) 66 | ->array(testedClass::await($generator))->isIdenticalTo([$value]) 67 | ; 68 | } 69 | 70 | /** @dataProvider valueDataProvider */ 71 | public function testAllValues($value, $otherValue = null) 72 | { 73 | $this 74 | ->given($otherValue = $otherValue ?? $value) 75 | ->then 76 | ->object($generator = testedClass::all($value, $otherValue))->isInstanceOf(\generator::class) 77 | ->array(testedClass::await($generator))->isIdenticalTo([$value, $otherValue]) 78 | ; 79 | } 80 | 81 | /** @dataProvider valueDataProvider */ 82 | public function testAllGenerator($value) 83 | { 84 | $this 85 | ->given($creator = function ($limit, $value) { 86 | while ($limit-- > 0) { 87 | yield; 88 | } 89 | 90 | return $value; 91 | }) 92 | ->then 93 | ->object($generator = testedClass::all($creator(3, $value)))->isInstanceOf(\generator::class) 94 | ->array(testedClass::await($generator))->isIdenticalTo([$value]) 95 | ; 96 | } 97 | 98 | /** @dataProvider valueDataProvider */ 99 | public function testAllGenerators($value, $otherValue = null) 100 | { 101 | $this 102 | ->given( 103 | $otherValue = $otherValue ?? $value, 104 | $creator = function ($limit, $value) { 105 | while ($limit-- > 0) { 106 | yield; 107 | } 108 | 109 | return $value; 110 | } 111 | ) 112 | ->then 113 | ->object($generator = testedClass::all($creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 114 | ->array(testedClass::await($generator))->isIdenticalTo([$value, $otherValue]) 115 | ; 116 | } 117 | 118 | /** @dataProvider valueDataProvider */ 119 | public function testAllGeneratorCreators($value, $otherValue = null) 120 | { 121 | $this 122 | ->given( 123 | $otherValue = $otherValue ?? $value, 124 | $creator = function ($limit, $value) { 125 | while ($limit-- > 0) { 126 | yield; 127 | } 128 | 129 | return (function () use ($value) { 130 | yield; 131 | 132 | return $value; 133 | })(); 134 | } 135 | ) 136 | ->then 137 | ->object($generator = testedClass::all($creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 138 | ->array(testedClass::await($generator))->isIdenticalTo([$value, $otherValue]) 139 | ; 140 | } 141 | 142 | /** @dataProvider valueDataProvider */ 143 | public function testRaceValues($value, $otherValue = null) 144 | { 145 | $this 146 | ->given($otherValue = $otherValue ?? $value) 147 | ->then 148 | ->object($generator = testedClass::race($value, $otherValue))->isInstanceOf(\generator::class) 149 | ->variable(testedClass::await($generator))->isIdenticalTo($value) 150 | ; 151 | } 152 | 153 | /** @dataProvider valueDataProvider */ 154 | public function testRaceGenerators($value, $otherValue = null) 155 | { 156 | $this 157 | ->given( 158 | $otherValue = $otherValue ?? $value, 159 | $creator = function ($limit, $value) { 160 | while ($limit-- > 0) { 161 | yield; 162 | } 163 | 164 | return $value; 165 | } 166 | ) 167 | ->then 168 | ->object($generator = testedClass::race($creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 169 | ->variable(testedClass::await($generator))->isIdenticalTo($otherValue) 170 | ; 171 | } 172 | 173 | /** @dataProvider valueDataProvider */ 174 | public function testRaceGeneratorCreators($value, $otherValue = null) 175 | { 176 | $this 177 | ->given( 178 | $otherValue = $otherValue ?? $value, 179 | $creator = function ($limit, $value) { 180 | while ($limit-- > 0) { 181 | yield; 182 | } 183 | 184 | return (function () use ($value) { 185 | yield; 186 | 187 | return $value; 188 | })(); 189 | } 190 | ) 191 | ->then 192 | ->object($generator = testedClass::race($creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 193 | ->variable(testedClass::await($generator))->isIdenticalTo($otherValue) 194 | ; 195 | } 196 | 197 | /** @dataProvider valueDataProvider */ 198 | public function testSomeValue($value) 199 | { 200 | $this 201 | ->object($generator = testedClass::some(1, $value))->isInstanceOf(\generator::class) 202 | ->array(testedClass::await($generator))->isIdenticalTo([$value]) 203 | ->exception(function () use (&$expected) { 204 | testedClass::await(testedClass::some($expected = rand(2, PHP_INT_MAX), uniqid())); 205 | } 206 | ) 207 | ->isInstanceOf(\logicException::class) 208 | ->hasMessage(sprintf('Some expected at least %d generators', $expected)) 209 | ; 210 | } 211 | 212 | /** @dataProvider valueDataProvider */ 213 | public function testSomeValues($value, $otherValue = null) 214 | { 215 | $this 216 | ->given($otherValue = $otherValue ?? $value) 217 | ->then 218 | ->object($generator = testedClass::some(1, $value, $otherValue))->isInstanceOf(\generator::class) 219 | ->variable(testedClass::await($generator))->isIdenticalTo([$value]) 220 | ->object($generator = testedClass::some(2, $value, $otherValue))->isInstanceOf(\generator::class) 221 | ->variable(testedClass::await($generator))->isIdenticalTo([$value, $otherValue]) 222 | ; 223 | } 224 | 225 | /** @dataProvider valueDataProvider */ 226 | public function testSomeGenerators($value, $otherValue = null) 227 | { 228 | $this 229 | ->given($creator = function ($limit, $value) { 230 | while ($limit-- > 0) { 231 | yield; 232 | } 233 | 234 | return $value; 235 | }) 236 | ->then 237 | ->object($generator = testedClass::some(1, $creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 238 | ->variable(testedClass::await($generator))->isIdenticalTo([$value]) 239 | ->object($generator = testedClass::some(2, $creator(3, $value), $creator(5, $otherValue)))->isInstanceOf(\generator::class) 240 | ->variable(testedClass::await($generator))->isIdenticalTo([$value, $otherValue]) 241 | ; 242 | } 243 | 244 | /** @dataProvider valueDataProvider */ 245 | public function testSomeGeneratorCreators($value, $otherValue = null) 246 | { 247 | $this 248 | ->given( 249 | $otherValue = $otherValue ?? $value, 250 | $creator = function ($limit, $value) { 251 | while ($limit-- > 0) { 252 | yield; 253 | } 254 | 255 | return (function () use ($value) { 256 | yield; 257 | 258 | return $value; 259 | })(); 260 | } 261 | ) 262 | ->then 263 | ->object($generator = testedClass::some(1, $creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 264 | ->variable(testedClass::await($generator))->isIdenticalTo([1 => $otherValue]) 265 | ->object($generator = testedClass::some(2, $creator(5, $value), $creator(3, $otherValue)))->isInstanceOf(\generator::class) 266 | ->variable(testedClass::await($generator))->isIdenticalTo([$value, $otherValue]) 267 | ; 268 | } 269 | 270 | protected function valueDataProvider() 271 | { 272 | return [ 273 | [rand(0, PHP_INT_MAX), rand(0, PHP_INT_MAX)], 274 | [1 / 3, 7 / 5], 275 | [uniqid(), uniqid()], 276 | [false, true], 277 | [true, false], 278 | [null], 279 | [range(0, 2), range(2, 4)], 280 | [new \stdClass(), new \stdClass()], 281 | ]; 282 | } 283 | } 284 | --------------------------------------------------------------------------------