pax_global_header00006660000000000000000000000064126100241600014502gustar00rootroot0000000000000052 comment=b1e1c0d55f8083c71eda2c28c12a228d708294ea promises-1.0.3/000077500000000000000000000000001261002416000133445ustar00rootroot00000000000000promises-1.0.3/.gitignore000066400000000000000000000002061261002416000153320ustar00rootroot00000000000000phpunit.xml composer.phar composer.lock composer-test.lock vendor/ build/artifacts/ artifacts/ docs/_build docs/*.pyc .idea .DS_STORE promises-1.0.3/.travis.yml000066400000000000000000000003411261002416000154530ustar00rootroot00000000000000language: php php: - 5.5 - 5.6 - 7.0 - hhvm sudo: false install: - travis_retry composer install --no-interaction --prefer-source script: make test matrix: allow_failures: - php: hhvm fast_finish: true promises-1.0.3/CHANGELOG.md000066400000000000000000000010701261002416000151530ustar00rootroot00000000000000# CHANGELOG ## 1.0.3 - 2015-10-15 * Update EachPromise to immediately resolve when the underlying promise iterator is empty. Previously, such a promise would throw an exception when its `wait` function was called. ## 1.0.2 - 2015-05-15 * Conditionally require functions.php. ## 1.0.1 - 2015-06-24 * Updating EachPromise to call next on the underlying promise iterator as late as possible to ensure that generators that generate new requests based on callbacks are not iterated until after callbacks are invoked. ## 1.0.0 - 2015-05-12 * Initial release promises-1.0.3/LICENSE000066400000000000000000000021271261002416000143530ustar00rootroot00000000000000Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. promises-1.0.3/Makefile000066400000000000000000000002751261002416000150100ustar00rootroot00000000000000all: clean test test: vendor/bin/phpunit coverage: vendor/bin/phpunit --coverage-html=artifacts/coverage view-coverage: open artifacts/coverage/index.html clean: rm -rf artifacts/* promises-1.0.3/README.md000066400000000000000000000351761261002416000146370ustar00rootroot00000000000000# Guzzle Promises [Promises/A+](https://promisesaplus.com/) implementation that handles promise chaining and resolution iteratively, allowing for "infinite" promise chaining while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/) for a general introduction to promises. - [Features](#features) - [Quick start](#quick-start) - [Synchronous wait](#synchronous-wait) - [Cancellation](#cancellation) - [API](#api) - [Promise](#promise) - [FulfilledPromise](#fulfilledpromise) - [RejectedPromise](#rejectedpromise) - [Promise interop](#promise-interop) - [Implementation notes](#implementation-notes) # Features - [Promises/A+](https://promisesaplus.com/) implementation. - Promise resolution and chaining is handled iteratively, allowing for "infinite" promise chaining. - Promises have a synchronous `wait` method. - Promises can be cancelled. - Works with any object that has a `then` function. - C# style async/await coroutine promises using `GuzzleHttp\Promise\coroutine()`. # Quick start A *promise* represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its `then` method, which registers callbacks to receive either a promise's eventual value or the reason why the promise cannot be fulfilled. ## Callbacks Callbacks are registered with the `then` method by providing an optional `$onFulfilled` followed by an optional `$onRejected` function. ```php use GuzzleHttp\Promise\Promise; $promise = new Promise(); $promise->then( // $onFulfilled function ($value) { echo 'The promise was fulfilled.'; }, // $onRejected function ($reason) { echo 'The promise was rejected.'; } ); ``` *Resolving* a promise means that you either fulfill a promise with a *value* or reject a promise with a *reason*. Resolving a promises triggers callbacks registered with the promises's `then` method. These callbacks are triggered only once and in the order in which they were added. ## Resolving a promise Promises are fulfilled using the `resolve($value)` method. Resolving a promise with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger all of the onFulfilled callbacks (resolving a promise with a rejected promise will reject the promise and trigger the `$onRejected` callbacks). ```php use GuzzleHttp\Promise\Promise; $promise = new Promise(); $promise ->then(function ($value) { // Return a value and don't break the chain return "Hello, " . $value; }) // This then is executed after the first then and receives the value // returned from the first then. ->then(function ($value) { echo $value; }); // Resolving the promise triggers the $onFulfilled callbacks and outputs // "Hello, reader". $promise->resolve('reader.'); ``` ## Promise forwarding Promises can be chained one after the other. Each then in the chain is a new promise. The return value of of a promise is what's forwarded to the next promise in the chain. Returning a promise in a `then` callback will cause the subsequent promises in the chain to only be fulfilled when the returned promise has been fulfilled. The next promise in the chain will be invoked with the resolved value of the promise. ```php use GuzzleHttp\Promise\Promise; $promise = new Promise(); $nextPromise = new Promise(); $promise ->then(function ($value) use ($nextPromise) { echo $value; return $nextPromise; }) ->then(function ($value) { echo $value; }); // Triggers the first callback and outputs "A" $promise->resolve('A'); // Triggers the second callback and outputs "B" $nextPromise->resolve('B'); ``` ## Promise rejection When a promise is rejected, the `$onRejected` callbacks are invoked with the rejection reason. ```php use GuzzleHttp\Promise\Promise; $promise = new Promise(); $promise->then(null, function ($reason) { echo $reason; }); $promise->reject('Error!'); // Outputs "Error!" ``` ## Rejection forwarding If an exception is thrown in an `$onRejected` callback, subsequent `$onRejected` callbacks are invoked with the thrown exception as the reason. ```php use GuzzleHttp\Promise\Promise; $promise = new Promise(); $promise->then(null, function ($reason) { throw new \Exception($reason); })->then(null, function ($reason) { assert($reason->getMessage() === 'Error!'); }); $promise->reject('Error!'); ``` You can also forward a rejection down the promise chain by returning a `GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or `$onRejected` callback. ```php use GuzzleHttp\Promise\Promise; use GuzzleHttp\Promise\RejectedPromise; $promise = new Promise(); $promise->then(null, function ($reason) { return new RejectedPromise($reason); })->then(null, function ($reason) { assert($reason === 'Error!'); }); $promise->reject('Error!'); ``` If an exception is not thrown in a `$onRejected` callback and the callback does not return a rejected promise, downstream `$onFulfilled` callbacks are invoked using the value returned from the `$onRejected` callback. ```php use GuzzleHttp\Promise\Promise; use GuzzleHttp\Promise\RejectedPromise; $promise = new Promise(); $promise ->then(null, function ($reason) { return "It's ok"; }) ->then(function ($value) { assert($value === "It's ok"); }); $promise->reject('Error!'); ``` # Synchronous wait You can synchronously force promises to complete using a promise's `wait` method. When creating a promise, you can provide a wait function that is used to synchronously force a promise to complete. When a wait function is invoked it is expected to deliver a value to the promise or reject the promise. If the wait function does not deliver a value, then an exception is thrown. The wait function provided to a promise constructor is invoked when the `wait` function of the promise is called. ```php $promise = new Promise(function () use (&$promise) { $promise->deliver('foo'); }); // Calling wait will return the value of the promise. echo $promise->wait(); // outputs "foo" ``` If an exception is encountered while invoking the wait function of a promise, the promise is rejected with the exception and the exception is thrown. ```php $promise = new Promise(function () use (&$promise) { throw new \Exception('foo'); }); $promise->wait(); // throws the exception. ``` Calling `wait` on a promise that has been fulfilled will not trigger the wait function. It will simply return the previously delivered value. ```php $promise = new Promise(function () { die('this is not called!'); }); $promise->deliver('foo'); echo $promise->wait(); // outputs "foo" ``` Calling `wait` on a promise that has been rejected will throw an exception. If the rejection reason is an instance of `\Exception` the reason is thrown. Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason can be obtained by calling the `getReason` method of the exception. ```php $promise = new Promise(); $promise->reject('foo'); $promise->wait(); ``` > PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo' ## Unwrapping a promise When synchronously waiting on a promise, you are joining the state of the promise into the current state of execution (i.e., return the value of the promise if it was fulfilled or throw an exception if it was rejected). This is called "unwrapping" the promise. Waiting on a promise will by default unwrap the promise state. You can force a promise to resolve and *not* unwrap the state of the promise by passing `false` to the first argument of the `wait` function: ```php $promise = new Promise(); $promise->reject('foo'); // This will not throw an exception. It simply ensures the promise has // been resolved. $promise->wait(false); ``` When unwrapping a promise, the delivered value of the promise will be waited upon until the unwrapped value is not a promise. This means that if you resolve promise A with a promise B and unwrap promise A, the value returned by the wait function will be the value delivered to promise B. **Note**: when you do not unwrap the promise, no value is returned. # Cancellation You can cancel a promise that has not yet been fulfilled using the `cancel()` method of a promise. When creating a promise you can provide an optional cancel function that when invoked cancels the action of computing a resolution of the promise. # API ## Promise When creating a promise object, you can provide an optional `$waitFn` and `$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is expected to resolve the promise. `$cancelFn` is a function with no arguments that is expected to cancel the computation of a promise. It is invoked when the `cancel()` method of a promise is called. ```php use GuzzleHttp\Promise\Promise; $promise = new Promise( function () use (&$promise) { $promise->resolve('waited'); }, function () { // do something that will cancel the promise computation (e.g., close // a socket, cancel a database query, etc...) } ); assert('waited' === $promise->wait()); ``` A promise has the following methods: - `then(callable $onFulfilled, callable $onRejected) : PromiseInterface` Creates a new promise that is fulfilled or rejected when the promise is resolved. - `wait($unwrap = true) : mixed` Synchronously waits on the promise to complete. `$unwrap` controls whether or not the value of the promise is returned for a fulfilled promise or if an exception is thrown if the promise is rejected. This is set to `true` by default. - `cancel()` Attempts to cancel the promise if possible. The promise being cancelled and the parent most ancestor that has not yet been resolved will also be cancelled. Any promises waiting on the cancelled promise to resolve will also be cancelled. - `getState() : string` Returns the state of the promise. One of `pending`, `fulfilled`, or `rejected`. - `resolve($value)` Fulfills the promise with the given `$value`. - `reject($reason)` Rejects the promise with the given `$reason`. ## FulfilledPromise A fulfilled promise can be created to represent a promise that has been fulfilled. ```php use GuzzleHttp\Promise\FulfilledPromise; $promise = new FulfilledPromise('value'); // Fulfilled callbacks are immediately invoked. $promise->then(function ($value) { echo $value; }); ``` ## RejectedPromise A rejected promise can be created to represent a promise that has been rejected. ```php use GuzzleHttp\Promise\RejectedPromise; $promise = new RejectedPromise('Error'); // Rejected callbacks are immediately invoked. $promise->then(null, function ($reason) { echo $reason; }); ``` # Promise interop This library works with foreign promises that have a `then` method. This means you can use Guzzle promises with [React promises](https://github.com/reactphp/promise) for example. When a foreign promise is returned inside of a then method callback, promise resolution will occur recursively. ```php // Create a React promise $deferred = new React\Promise\Deferred(); $reactPromise = $deferred->promise(); // Create a Guzzle promise that is fulfilled with a React promise. $guzzlePromise = new \GuzzleHttp\Promise\Promise(); $guzzlePromise->then(function ($value) use ($reactPromise) { // Do something something with the value... // Return the React promise return $reactPromise; }); ``` Please note that wait and cancel chaining is no longer possible when forwarding a foreign promise. You will need to wrap a third-party promise with a Guzzle promise in order to utilize wait and cancel functions with foreign promises. ## Event Loop Integration In order to keep the stack size constant, Guzzle promises are resolved asynchronously using a task queue. When waiting on promises synchronously, the task queue will be automatically run to ensure that the blocking promise and any forwarded promises are resolved. When using promises asynchronously in an event loop, you will need to run the task queue on each tick of the loop. If you do not run the task queue, then promises will not be resolved. You can run the task queue using the `run()` method of the global task queue instance. ```php // Get the global task queue $queue = \GuzzleHttp\Promise\queue(); $queue->run(); ``` For example, you could use Guzzle promises with React using a periodic timer: ```php $loop = React\EventLoop\Factory::create(); $loop->addPeriodicTimer(0, [$queue, 'run']); ``` *TODO*: Perhaps adding a `futureTick()` on each tick would be faster? # Implementation notes ## Promise resolution and chaining is handled iteratively By shuffling pending handlers from one owner to another, promises are resolved iteratively, allowing for "infinite" then chaining. ```php then(function ($v) { // The stack size remains constant (a good thing) echo xdebug_get_stack_depth() . ', '; return $v + 1; }); } $parent->resolve(0); var_dump($p->wait()); // int(1000) ``` When a promise is fulfilled or rejected with a non-promise value, the promise then takes ownership of the handlers of each child promise and delivers values down the chain without using recursion. When a promise is resolved with another promise, the original promise transfers all of its pending handlers to the new promise. When the new promise is eventually resolved, all of the pending handlers are delivered the forwarded value. ## A promise is the deferred. Some promise libraries implement promises using a deferred object to represent a computation and a promise object to represent the delivery of the result of the computation. This is a nice separation of computation and delivery because consumers of the promise cannot modify the value that will be eventually delivered. One side effect of being able to implement promise resolution and chaining iteratively is that you need to be able for one promise to reach into the state of another promise to shuffle around ownership of handlers. In order to achieve this without making the handlers of a promise publicly mutable, a promise is also the deferred value, allowing promises of the same parent class to reach into and modify the private properties of promises of the same type. While this does allow consumers of the value to modify the resolution or rejection of the deferred, it is a small price to pay for keeping the stack size constant. ```php $promise = new Promise(); $promise->then(function ($value) { echo $value; }); // The promise is the deferred value, so you can deliver a value to it. $promise->deliver('foo'); // prints "foo" ``` promises-1.0.3/composer.json000066400000000000000000000012631261002416000160700ustar00rootroot00000000000000{ "name": "guzzlehttp/promises", "type": "library", "description": "Guzzle promises library", "keywords": ["promise"], "license": "MIT", "authors": [ { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" } ], "require": { "php": ">=5.5.0" }, "require-dev": { "phpunit/phpunit": "~4.0" }, "autoload": { "psr-4": { "GuzzleHttp\\Promise\\": "src/" }, "files": ["src/functions_include.php"] }, "extra": { "branch-alias": { "dev-master": "1.0-dev" } } } promises-1.0.3/phpunit.xml.dist000066400000000000000000000006331261002416000165210ustar00rootroot00000000000000 tests src src/ promises-1.0.3/src/000077500000000000000000000000001261002416000141335ustar00rootroot00000000000000promises-1.0.3/src/AggregateException.php000066400000000000000000000005731261002416000204160ustar00rootroot00000000000000iterable = iter_for($iterable); if (isset($config['concurrency'])) { $this->concurrency = $config['concurrency']; } if (isset($config['fulfilled'])) { $this->onFulfilled = $config['fulfilled']; } if (isset($config['rejected'])) { $this->onRejected = $config['rejected']; } } public function promise() { if ($this->aggregate) { return $this->aggregate; } try { $this->createPromise(); $this->iterable->rewind(); $this->refillPending(); } catch (\Exception $e) { $this->aggregate->reject($e); } return $this->aggregate; } private function createPromise() { $this->aggregate = new Promise(function () { reset($this->pending); if (empty($this->pending) && !$this->iterable->valid()) { $this->aggregate->resolve(null); return; } // Consume a potentially fluctuating list of promises while // ensuring that indexes are maintained (precluding array_shift). while ($promise = current($this->pending)) { next($this->pending); $promise->wait(); if ($this->aggregate->getState() !== PromiseInterface::PENDING) { return; } } }); // Clear the references when the promise is resolved. $clearFn = function () { $this->iterable = $this->concurrency = $this->pending = null; $this->onFulfilled = $this->onRejected = null; }; $this->aggregate->then($clearFn, $clearFn); } private function refillPending() { if (!$this->concurrency) { // Add all pending promises. while ($this->addPending() && $this->advanceIterator()); return; } // Add only up to N pending promises. $concurrency = is_callable($this->concurrency) ? call_user_func($this->concurrency, count($this->pending)) : $this->concurrency; $concurrency = max($concurrency - count($this->pending), 0); // Concurrency may be set to 0 to disallow new promises. if (!$concurrency) { return; } // Add the first pending promise. $this->addPending(); // Note this is special handling for concurrency=1 so that we do // not advance the iterator after adding the first promise. This // helps work around issues with generators that might not have the // next value to yield until promise callbacks are called. while (--$concurrency && $this->advanceIterator() && $this->addPending()); } private function addPending() { if (!$this->iterable || !$this->iterable->valid()) { return false; } $promise = promise_for($this->iterable->current()); $idx = $this->iterable->key(); $this->pending[$idx] = $promise->then( function ($value) use ($idx) { if ($this->onFulfilled) { call_user_func( $this->onFulfilled, $value, $idx, $this->aggregate ); } $this->step($idx); }, function ($reason) use ($idx) { if ($this->onRejected) { call_user_func( $this->onRejected, $reason, $idx, $this->aggregate ); } $this->step($idx); } ); return true; } private function advanceIterator() { try { $this->iterable->next(); return true; } catch (\Exception $e) { $this->aggregate->reject($e); return false; } } private function step($idx) { // If the promise was already resolved, then ignore this step. if ($this->aggregate->getState() !== PromiseInterface::PENDING) { return; } unset($this->pending[$idx]); $this->advanceIterator(); if (!$this->checkIfFinished()) { // Add more pending promises if possible. $this->refillPending(); } } private function checkIfFinished() { if (!$this->pending && !$this->iterable->valid()) { // Resolve the promise if there's nothing left to do. $this->aggregate->resolve(null); return true; } return false; } } promises-1.0.3/src/FulfilledPromise.php000066400000000000000000000035401261002416000201130ustar00rootroot00000000000000value = $value; } public function then( callable $onFulfilled = null, callable $onRejected = null ) { // Return itself if there is no onFulfilled function. if (!$onFulfilled) { return $this; } $queue = queue(); $p = new Promise([$queue, 'run']); $value = $this->value; $queue->add(static function () use ($p, $value, $onFulfilled) { if ($p->getState() === self::PENDING) { try { $p->resolve($onFulfilled($value)); } catch (\Exception $e) { $p->reject($e); } } }); return $p; } public function otherwise(callable $onRejected) { return $this->then(null, $onRejected); } public function wait($unwrap = true, $defaultDelivery = null) { return $unwrap ? $this->value : null; } public function getState() { return self::FULFILLED; } public function resolve($value) { if ($value !== $this->value) { throw new \LogicException("Cannot resolve a fulfilled promise"); } } public function reject($reason) { throw new \LogicException("Cannot reject a fulfilled promise"); } public function cancel() { // pass } } promises-1.0.3/src/Promise.php000066400000000000000000000202311261002416000162600ustar00rootroot00000000000000waitFn = $waitFn; $this->cancelFn = $cancelFn; } public function then( callable $onFulfilled = null, callable $onRejected = null ) { if ($this->state === self::PENDING) { $p = new Promise(null, [$this, 'cancel']); $this->handlers[] = [$p, $onFulfilled, $onRejected]; $p->waitList = $this->waitList; $p->waitList[] = $this; return $p; } // Return a fulfilled promise and immediately invoke any callbacks. if ($this->state === self::FULFILLED) { return $onFulfilled ? promise_for($this->result)->then($onFulfilled) : promise_for($this->result); } // It's either cancelled or rejected, so return a rejected promise // and immediately invoke any callbacks. $rejection = rejection_for($this->result); return $onRejected ? $rejection->then(null, $onRejected) : $rejection; } public function otherwise(callable $onRejected) { return $this->then(null, $onRejected); } public function wait($unwrap = true) { $this->waitIfPending(); if (!$unwrap) { return null; } if ($this->result instanceof PromiseInterface) { return $this->result->wait($unwrap); } elseif ($this->state === self::FULFILLED) { return $this->result; } else { // It's rejected so "unwrap" and throw an exception. throw exception_for($this->result); } } public function getState() { return $this->state; } public function cancel() { if ($this->state !== self::PENDING) { return; } $this->waitFn = $this->waitList = null; if ($this->cancelFn) { $fn = $this->cancelFn; $this->cancelFn = null; try { $fn(); } catch (\Exception $e) { $this->reject($e); } } // Reject the promise only if it wasn't rejected in a then callback. if ($this->state === self::PENDING) { $this->reject(new CancellationException('Promise has been cancelled')); } } public function resolve($value) { $this->settle(self::FULFILLED, $value); } public function reject($reason) { $this->settle(self::REJECTED, $reason); } private function settle($state, $value) { if ($this->state !== self::PENDING) { // Ignore calls with the same resolution. if ($state === $this->state && $value === $this->result) { return; } throw $this->state === $state ? new \LogicException("The promise is already {$state}.") : new \LogicException("Cannot change a {$this->state} promise to {$state}"); } if ($value === $this) { throw new \LogicException('Cannot fulfill or reject a promise with itself'); } // Clear out the state of the promise but stash the handlers. $this->state = $state; $this->result = $value; $handlers = $this->handlers; $this->handlers = null; $this->waitList = $this->waitFn = null; $this->cancelFn = null; if (!$handlers) { return; } // If the value was not a settled promise or a thenable, then resolve // it in the task queue using the correct ID. if (!method_exists($value, 'then')) { $id = $state === self::FULFILLED ? 1 : 2; // It's a success, so resolve the handlers in the queue. queue()->add(static function () use ($id, $value, $handlers) { foreach ($handlers as $handler) { self::callHandler($id, $value, $handler); } }); } elseif ($value instanceof Promise && $value->getState() === self::PENDING ) { // We can just merge our handlers onto the next promise. $value->handlers = array_merge($value->handlers, $handlers); } else { // Resolve the handlers when the forwarded promise is resolved. $value->then( static function ($value) use ($handlers) { foreach ($handlers as $handler) { self::callHandler(1, $value, $handler); } }, static function ($reason) use ($handlers) { foreach ($handlers as $handler) { self::callHandler(2, $reason, $handler); } } ); } } /** * Call a stack of handlers using a specific callback index and value. * * @param int $index 1 (resolve) or 2 (reject). * @param mixed $value Value to pass to the callback. * @param array $handler Array of handler data (promise and callbacks). * * @return array Returns the next group to resolve. */ private static function callHandler($index, $value, array $handler) { /** @var PromiseInterface $promise */ $promise = $handler[0]; // The promise may have been cancelled or resolved before placing // this thunk in the queue. if ($promise->getState() !== self::PENDING) { return; } try { if (isset($handler[$index])) { $promise->resolve($handler[$index]($value)); } elseif ($index === 1) { // Forward resolution values as-is. $promise->resolve($value); } else { // Forward rejections down the chain. $promise->reject($value); } } catch (\Exception $reason) { $promise->reject($reason); } } private function waitIfPending() { if ($this->state !== self::PENDING) { return; } elseif ($this->waitFn) { $this->invokeWaitFn(); } elseif ($this->waitList) { $this->invokeWaitList(); } else { // If there's not wait function, then reject the promise. $this->reject('Cannot wait on a promise that has ' . 'no internal wait function. You must provide a wait ' . 'function when constructing the promise to be able to ' . 'wait on a promise.'); } queue()->run(); if ($this->state === self::PENDING) { $this->reject('Invoking the wait callback did not resolve the promise'); } } private function invokeWaitFn() { try { $wfn = $this->waitFn; $this->waitFn = null; $wfn(true); } catch (\Exception $reason) { if ($this->state === self::PENDING) { // The promise has not been resolved yet, so reject the promise // with the exception. $this->reject($reason); } else { // The promise was already resolved, so there's a problem in // the application. throw $reason; } } } private function invokeWaitList() { $waitList = $this->waitList; $this->waitList = null; foreach ($waitList as $result) { descend: $result->waitIfPending(); if ($result->result instanceof Promise) { $result = $result->result; goto descend; } } } } promises-1.0.3/src/PromiseInterface.php000066400000000000000000000054161261002416000201110ustar00rootroot00000000000000reason = $reason; } public function then( callable $onFulfilled = null, callable $onRejected = null ) { // If there's no onRejected callback then just return self. if (!$onRejected) { return $this; } $queue = queue(); $reason = $this->reason; $p = new Promise([$queue, 'run']); $queue->add(static function () use ($p, $reason, $onRejected) { if ($p->getState() === self::PENDING) { try { // Return a resolved promise if onRejected does not throw. $p->resolve($onRejected($reason)); } catch (\Exception $e) { // onRejected threw, so return a rejected promise. $p->reject($e); } } }); return $p; } public function otherwise(callable $onRejected) { return $this->then(null, $onRejected); } public function wait($unwrap = true, $defaultDelivery = null) { if ($unwrap) { throw exception_for($this->reason); } } public function getState() { return self::REJECTED; } public function resolve($value) { throw new \LogicException("Cannot resolve a rejected promise"); } public function reject($reason) { if ($reason !== $this->reason) { throw new \LogicException("Cannot reject a rejected promise"); } } public function cancel() { // pass } } promises-1.0.3/src/RejectionException.php000066400000000000000000000023011261002416000204410ustar00rootroot00000000000000reason = $reason; $message = 'The promise was rejected'; if ($description) { $message .= ' with reason: ' . $description; } elseif (is_string($reason) || (is_object($reason) && method_exists($reason, '__toString')) ) { $message .= ' with reason: ' . $this->reason; } elseif ($reason instanceof \JsonSerializable) { $message .= ' with reason: ' . json_encode($this->reason, JSON_PRETTY_PRINT); } parent::__construct($message); } /** * Returns the rejection reason. * * @return mixed */ public function getReason() { return $this->reason; } } promises-1.0.3/src/TaskQueue.php000066400000000000000000000041601261002416000165540ustar00rootroot00000000000000run(); */ class TaskQueue { private $enableShutdown = true; private $queue = []; public function __construct($withShutdown = true) { if ($withShutdown) { register_shutdown_function(function () { if ($this->enableShutdown) { // Only run the tasks if an E_ERROR didn't occur. $err = error_get_last(); if (!$err || ($err['type'] ^ E_ERROR)) { $this->run(); } } }); } } /** * Returns true if the queue is empty. * * @return bool */ public function isEmpty() { return !$this->queue; } /** * Adds a task to the queue that will be executed the next time run is * called. * * @param callable $task */ public function add(callable $task) { $this->queue[] = $task; } /** * Execute all of the pending task in the queue. */ public function run() { while ($task = array_shift($this->queue)) { $task(); } } /** * The task queue will be run and exhausted by default when the process * exits IFF the exit is not the result of a PHP E_ERROR error. * * You can disable running the automatic shutdown of the queue by calling * this function. If you disable the task queue shutdown process, then you * MUST either run the task queue (as a result of running your event loop * or manually using the run() method) or wait on each outstanding promise. * * Note: This shutdown will occur before any destructors are triggered. */ public function disableShutdown() { $this->enableShutdown = false; } } promises-1.0.3/src/functions.php000066400000000000000000000320021261002416000166510ustar00rootroot00000000000000 * while ($eventLoop->isRunning()) { * GuzzleHttp\Promise\queue()->run(); * } * * * @return TaskQueue */ function queue() { static $queue; if (!$queue) { $queue = new TaskQueue(); } return $queue; } /** * Adds a function to run in the task queue when it is next `run()` and returns * a promise that is fulfilled or rejected with the result. * * @param callable $task Task function to run. * * @return PromiseInterface */ function task(callable $task) { $queue = queue(); $promise = new Promise([$queue, 'run']); $queue->add(function () use ($task, $promise) { try { $promise->resolve($task()); } catch (\Exception $e) { $promise->reject($e); } }); return $promise; } /** * Creates a promise for a value if the value is not a promise. * * @param mixed $value Promise or value. * * @return PromiseInterface */ function promise_for($value) { if ($value instanceof PromiseInterface) { return $value; } // Return a Guzzle promise that shadows the given promise. if (method_exists($value, 'then')) { $wfn = method_exists($value, 'wait') ? [$value, 'wait'] : null; $cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null; $promise = new Promise($wfn, $cfn); $value->then([$promise, 'resolve'], [$promise, 'reject']); return $promise; } return new FulfilledPromise($value); } /** * Creates a rejected promise for a reason if the reason is not a promise. If * the provided reason is a promise, then it is returned as-is. * * @param mixed $reason Promise or reason. * * @return PromiseInterface */ function rejection_for($reason) { if ($reason instanceof PromiseInterface) { return $reason; } return new RejectedPromise($reason); } /** * Create an exception for a rejected promise value. * * @param mixed $reason * * @return \Exception */ function exception_for($reason) { return $reason instanceof \Exception ? $reason : new RejectionException($reason); } /** * Returns an iterator for the given value. * * @param mixed $value * * @return \Iterator */ function iter_for($value) { if ($value instanceof \Iterator) { return $value; } elseif (is_array($value)) { return new \ArrayIterator($value); } else { return new \ArrayIterator([$value]); } } /** * Synchronously waits on a promise to resolve and returns an inspection state * array. * * Returns a state associative array containing a "state" key mapping to a * valid promise state. If the state of the promise is "fulfilled", the array * will contain a "value" key mapping to the fulfilled value of the promise. If * the promise is rejected, the array will contain a "reason" key mapping to * the rejection reason of the promise. * * @param PromiseInterface $promise Promise or value. * * @return array */ function inspect(PromiseInterface $promise) { try { return [ 'state' => PromiseInterface::FULFILLED, 'value' => $promise->wait() ]; } catch (RejectionException $e) { return ['state' => 'rejected', 'reason' => $e->getReason()]; } catch (\Exception $e) { return ['state' => 'rejected', 'reason' => $e]; } } /** * Waits on all of the provided promises, but does not unwrap rejected promises * as thrown exception. * * Returns an array of inspection state arrays. * * @param PromiseInterface[] $promises Traversable of promises to wait upon. * * @return array * @see GuzzleHttp\Promise\inspect for the inspection state array format. */ function inspect_all($promises) { $results = []; foreach ($promises as $key => $promise) { $results[$key] = inspect($promise); } return $results; } /** * Waits on all of the provided promises and returns the fulfilled values. * * Returns an array that contains the value of each promise (in the same order * the promises were provided). An exception is thrown if any of the promises * are rejected. * * @param mixed $promises Iterable of PromiseInterface objects to wait on. * * @return array * @throws \Exception on error */ function unwrap($promises) { $results = []; foreach ($promises as $key => $promise) { $results[$key] = $promise->wait(); } return $results; } /** * Given an array of promises, return a promise that is fulfilled when all the * items in the array are fulfilled. * * The promise's fulfillment value is an array with fulfillment values at * respective positions to the original array. If any promise in the array * rejects, the returned promise is rejected with the rejection reason. * * @param mixed $promises Promises or values. * * @return Promise */ function all($promises) { $results = []; return each( $promises, function ($value, $idx) use (&$results) { $results[$idx] = $value; }, function ($reason, $idx, Promise $aggregate) { $aggregate->reject($reason); } )->then(function () use (&$results) { ksort($results); return $results; }); } /** * Initiate a competitive race between multiple promises or values (values will * become immediately fulfilled promises). * * When count amount of promises have been fulfilled, the returned promise is * fulfilled with an array that contains the fulfillment values of the winners * in order of resolution. * * This prommise is rejected with a {@see GuzzleHttp\Promise\AggregateException} * if the number of fulfilled promises is less than the desired $count. * * @param int $count Total number of promises. * @param mixed $promises Promises or values. * * @return Promise */ function some($count, $promises) { $results = []; $rejections = []; return each( $promises, function ($value, $idx, PromiseInterface $p) use (&$results, $count) { if ($p->getState() !== PromiseInterface::PENDING) { return; } $results[$idx] = $value; if (count($results) >= $count) { $p->resolve(null); } }, function ($reason) use (&$rejections) { $rejections[] = $reason; } )->then( function () use (&$results, &$rejections, $count) { if (count($results) !== $count) { throw new AggregateException( 'Not enough promises to fulfill count', $rejections ); } ksort($results); return array_values($results); } ); } /** * Like some(), with 1 as count. However, if the promise fulfills, the * fulfillment value is not an array of 1 but the value directly. * * @param mixed $promises Promises or values. * * @return PromiseInterface */ function any($promises) { return some(1, $promises)->then(function ($values) { return $values[0]; }); } /** * Returns a promise that is fulfilled when all of the provided promises have * been fulfilled or rejected. * * The returned promise is fulfilled with an array of inspection state arrays. * * @param mixed $promises Promises or values. * * @return Promise * @see GuzzleHttp\Promise\inspect for the inspection state array format. */ function settle($promises) { $results = []; return each( $promises, function ($value, $idx) use (&$results) { $results[$idx] = ['state' => 'fulfilled', 'value' => $value]; }, function ($reason, $idx) use (&$results) { $results[$idx] = ['state' => 'rejected', 'reason' => $reason]; } )->then(function () use (&$results) { ksort($results); return $results; }); } /** * Given an iterator that yields promises or values, returns a promise that is * fulfilled with a null value when the iterator has been consumed or the * aggregate promise has been fulfilled or rejected. * * $onFulfilled is a function that accepts the fulfilled value, iterator * index, and the aggregate promise. The callback can invoke any necessary side * effects and choose to resolve or reject the aggregate promise if needed. * * $onRejected is a function that accepts the rejection reason, iterator * index, and the aggregate promise. The callback can invoke any necessary side * effects and choose to resolve or reject the aggregate promise if needed. * * @param mixed $iterable Iterator or array to iterate over. * @param callable $onFulfilled * @param callable $onRejected * * @return Promise */ function each( $iterable, callable $onFulfilled = null, callable $onRejected = null ) { return (new EachPromise($iterable, [ 'fulfilled' => $onFulfilled, 'rejected' => $onRejected ]))->promise(); } /** * Like each, but only allows a certain number of outstanding promises at any * given time. * * $concurrency may be an integer or a function that accepts the number of * pending promises and returns a numeric concurrency limit value to allow for * dynamic a concurrency size. * * @param mixed $iterable * @param int|callable $concurrency * @param callable $onFulfilled * @param callable $onRejected * * @return mixed */ function each_limit( $iterable, $concurrency, callable $onFulfilled = null, callable $onRejected = null ) { return (new EachPromise($iterable, [ 'fulfilled' => $onFulfilled, 'rejected' => $onRejected, 'concurrency' => $concurrency ]))->promise(); } /** * Like each_limit, but ensures that no promise in the given $iterable argument * is rejected. If any promise is rejected, then the aggregate promise is * rejected with the encountered rejection. * * @param mixed $iterable * @param int|callable $concurrency * @param callable $onFulfilled * * @return mixed */ function each_limit_all( $iterable, $concurrency, callable $onFulfilled = null ) { return each_limit( $iterable, $concurrency, $onFulfilled, function ($reason, $idx, PromiseInterface $aggregate) { $aggregate->reject($reason); } ); } /** * Returns true if a promise is fulfilled. * * @param PromiseInterface $promise * * @return bool */ function is_fulfilled(PromiseInterface $promise) { return $promise->getState() === PromiseInterface::FULFILLED; } /** * Returns true if a promise is rejected. * * @param PromiseInterface $promise * * @return bool */ function is_rejected(PromiseInterface $promise) { return $promise->getState() === PromiseInterface::REJECTED; } /** * Returns true if a promise is fulfilled or rejected. * * @param PromiseInterface $promise * * @return bool */ function is_settled(PromiseInterface $promise) { return $promise->getState() !== PromiseInterface::PENDING; } /** * Creates a promise that is resolved using a generator that yields values or * promises (somewhat similar to C#'s async keyword). * * When called, the coroutine function will start an instance of the generator * and returns a promise that is fulfilled with its final yielded value. * * Control is returned back to the generator when the yielded promise settles. * This can lead to less verbose code when doing lots of sequential async calls * with minimal processing in between. * * use GuzzleHttp\Promise; * * function createPromise($value) { * return new Promise\FulfilledPromise($value); * } * * $promise = Promise\coroutine(function () { * $value = (yield createPromise('a')); * try { * $value = (yield createPromise($value . 'b')); * } catch (\Exception $e) { * // The promise was rejected. * } * yield $value . 'c'; * }); * * // Outputs "abc" * $promise->then(function ($v) { echo $v; }); * * @param callable $generatorFn Generator function to wrap into a promise. * * @return Promise * @link https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration */ function coroutine(callable $generatorFn) { $generator = $generatorFn(); return __next_coroutine($generator->current(), $generator)->then(); } /** @internal */ function __next_coroutine($yielded, \Generator $generator) { return promise_for($yielded)->then( function ($value) use ($generator) { $nextYield = $generator->send($value); return $generator->valid() ? __next_coroutine($nextYield, $generator) : $value; }, function ($reason) use ($generator) { $nextYield = $generator->throw(exception_for($reason)); // The throw was caught, so keep iterating on the coroutine return __next_coroutine($nextYield, $generator); } ); } promises-1.0.3/src/functions_include.php000066400000000000000000000002471261002416000203620ustar00rootroot00000000000000assertContains('foo', $e->getMessage()); $this->assertEquals(['baz', 'bar'], $e->getReason()); } } promises-1.0.3/tests/EachPromiseTest.php000066400000000000000000000241531261002416000202630ustar00rootroot00000000000000 100]); $this->assertSame($each->promise(), $each->promise()); } public function testInvokesAllPromises() { $promises = [new Promise(), new Promise(), new Promise()]; $called = []; $each = new EachPromise($promises, [ 'fulfilled' => function ($value) use (&$called) { $called[] = $value; } ]); $p = $each->promise(); $promises[0]->resolve('a'); $promises[1]->resolve('c'); $promises[2]->resolve('b'); P\queue()->run(); $this->assertEquals(['a', 'c', 'b'], $called); $this->assertEquals(PromiseInterface::FULFILLED, $p->getState()); } public function testIsWaitable() { $a = new Promise(function () use (&$a) { $a->resolve('a'); }); $b = new Promise(function () use (&$b) { $b->resolve('b'); }); $called = []; $each = new EachPromise([$a, $b], [ 'fulfilled' => function ($value) use (&$called) { $called[] = $value; } ]); $p = $each->promise(); $this->assertNull($p->wait()); $this->assertEquals(PromiseInterface::FULFILLED, $p->getState()); $this->assertEquals(['a', 'b'], $called); } public function testCanResolveBeforeConsumingAll() { $called = 0; $a = new Promise(function () use (&$a) { $a->resolve('a'); }); $b = new Promise(function () { $this->fail(); }); $each = new EachPromise([$a, $b], [ 'fulfilled' => function ($value, $idx, Promise $aggregate) use (&$called) { $this->assertSame($idx, 0); $this->assertEquals('a', $value); $aggregate->resolve(null); $called++; }, 'rejected' => function (\Exception $reason) { $this->fail($reason->getMessage()); } ]); $p = $each->promise(); $p->wait(); $this->assertNull($p->wait()); $this->assertEquals(1, $called); $this->assertEquals(PromiseInterface::FULFILLED, $a->getState()); $this->assertEquals(PromiseInterface::PENDING, $b->getState()); // Resolving $b has no effect on the aggregate promise. $b->resolve('foo'); $this->assertEquals(1, $called); } public function testLimitsPendingPromises() { $pending = [new Promise(), new Promise(), new Promise(), new Promise()]; $promises = new \ArrayIterator($pending); $each = new EachPromise($promises, ['concurrency' => 2]); $p = $each->promise(); $this->assertCount(2, $this->readAttribute($each, 'pending')); $pending[0]->resolve('a'); $this->assertCount(2, $this->readAttribute($each, 'pending')); $this->assertTrue($promises->valid()); $pending[1]->resolve('b'); P\queue()->run(); $this->assertCount(2, $this->readAttribute($each, 'pending')); $this->assertTrue($promises->valid()); $promises[2]->resolve('c'); P\queue()->run(); $this->assertCount(1, $this->readAttribute($each, 'pending')); $this->assertEquals(PromiseInterface::PENDING, $p->getState()); $promises[3]->resolve('d'); P\queue()->run(); $this->assertNull($this->readAttribute($each, 'pending')); $this->assertEquals(PromiseInterface::FULFILLED, $p->getState()); $this->assertFalse($promises->valid()); } public function testDynamicallyLimitsPendingPromises() { $calls = []; $pendingFn = function ($count) use (&$calls) { $calls[] = $count; return 2; }; $pending = [new Promise(), new Promise(), new Promise(), new Promise()]; $promises = new \ArrayIterator($pending); $each = new EachPromise($promises, ['concurrency' => $pendingFn]); $p = $each->promise(); $this->assertCount(2, $this->readAttribute($each, 'pending')); $pending[0]->resolve('a'); $this->assertCount(2, $this->readAttribute($each, 'pending')); $this->assertTrue($promises->valid()); $pending[1]->resolve('b'); $this->assertCount(2, $this->readAttribute($each, 'pending')); P\queue()->run(); $this->assertTrue($promises->valid()); $promises[2]->resolve('c'); P\queue()->run(); $this->assertCount(1, $this->readAttribute($each, 'pending')); $this->assertEquals(PromiseInterface::PENDING, $p->getState()); $promises[3]->resolve('d'); P\queue()->run(); $this->assertNull($this->readAttribute($each, 'pending')); $this->assertEquals(PromiseInterface::FULFILLED, $p->getState()); $this->assertEquals([0, 1, 1, 1], $calls); $this->assertFalse($promises->valid()); } public function testClearsReferencesWhenResolved() { $called = false; $a = new Promise(function () use (&$a, &$called) { $a->resolve('a'); $called = true; }); $each = new EachPromise([$a], [ 'concurrency' => function () { return 1; }, 'fulfilled' => function () {}, 'rejected' => function () {} ]); $each->promise()->wait(); $this->assertNull($this->readAttribute($each, 'onFulfilled')); $this->assertNull($this->readAttribute($each, 'onRejected')); $this->assertNull($this->readAttribute($each, 'iterable')); $this->assertNull($this->readAttribute($each, 'pending')); $this->assertNull($this->readAttribute($each, 'concurrency')); $this->assertTrue($called); } public function testCanBeCancelled() { $this->markTestIncomplete(); } public function testFulfillsImmediatelyWhenGivenAnEmptyIterator() { $each = new EachPromise(new \ArrayIterator([])); $result = $each->promise()->wait(); } public function testDoesNotBlowStackWithFulfilledPromises() { $pending = []; for ($i = 0; $i < 100; $i++) { $pending[] = new FulfilledPromise($i); } $values = []; $each = new EachPromise($pending, [ 'fulfilled' => function ($value) use (&$values) { $values[] = $value; } ]); $called = false; $each->promise()->then(function () use (&$called) { $called = true; }); $this->assertFalse($called); P\queue()->run(); $this->assertTrue($called); $this->assertEquals(range(0, 99), $values); } public function testDoesNotBlowStackWithRejectedPromises() { $pending = []; for ($i = 0; $i < 100; $i++) { $pending[] = new RejectedPromise($i); } $values = []; $each = new EachPromise($pending, [ 'rejected' => function ($value) use (&$values) { $values[] = $value; } ]); $called = false; $each->promise()->then( function () use (&$called) { $called = true; }, function () { $this->fail('Should not have rejected.'); } ); $this->assertFalse($called); P\queue()->run(); $this->assertTrue($called); $this->assertEquals(range(0, 99), $values); } public function testReturnsPromiseForWhatever() { $called = []; $arr = ['a', 'b']; $each = new EachPromise($arr, [ 'fulfilled' => function ($v) use (&$called) { $called[] = $v; } ]); $p = $each->promise(); $this->assertNull($p->wait()); $this->assertEquals(['a', 'b'], $called); } public function testRejectsAggregateWhenNextThrows() { $iter = function () { yield 'a'; throw new \Exception('Failure'); }; $each = new EachPromise($iter()); $p = $each->promise(); $e = null; $received = null; $p->then(null, function ($reason) use (&$e) { $e = $reason; }); P\queue()->run(); $this->assertInstanceOf('Exception', $e); $this->assertEquals('Failure', $e->getMessage()); } public function testDoesNotCallNextOnIteratorUntilNeededWhenWaiting() { $results = []; $values = [10]; $remaining = 9; $iter = function () use (&$values) { while ($value = array_pop($values)) { yield $value; } }; $each = new EachPromise($iter(), [ 'concurrency' => 1, 'fulfilled' => function ($r) use (&$results, &$values, &$remaining) { $results[] = $r; if ($remaining > 0) { $values[] = $remaining--; } } ]); $each->promise()->wait(); $this->assertEquals(range(10, 1), $results); } public function testDoesNotCallNextOnIteratorUntilNeededWhenAsync() { $firstPromise = new Promise(); $pending = [$firstPromise]; $values = [$firstPromise]; $results = []; $remaining = 9; $iter = function () use (&$values) { while ($value = array_pop($values)) { yield $value; } }; $each = new EachPromise($iter(), [ 'concurrency' => 1, 'fulfilled' => function ($r) use (&$results, &$values, &$remaining, &$pending) { $results[] = $r; if ($remaining-- > 0) { $pending[] = $values[] = new Promise(); } } ]); $i = 0; $each->promise(); while ($promise = array_pop($pending)) { $promise->resolve($i++); P\queue()->run(); } $this->assertEquals(range(0, 9), $results); } } promises-1.0.3/tests/FulfilledPromiseTest.php000066400000000000000000000055111261002416000213260ustar00rootroot00000000000000assertEquals('fulfilled', $p->getState()); $this->assertEquals('foo', $p->wait(true)); } public function testCannotCancel() { $p = new FulfilledPromise('foo'); $this->assertEquals('fulfilled', $p->getState()); $p->cancel(); $this->assertEquals('foo', $p->wait()); } /** * @expectedException \LogicException * @exepctedExceptionMessage Cannot resolve a fulfilled promise */ public function testCannotResolve() { $p = new FulfilledPromise('foo'); $p->resolve('bar'); } /** * @expectedException \LogicException * @exepctedExceptionMessage Cannot reject a fulfilled promise */ public function testCannotReject() { $p = new FulfilledPromise('foo'); $p->reject('bar'); } public function testCanResolveWithSameValue() { $p = new FulfilledPromise('foo'); $p->resolve('foo'); } /** * @expectedException \InvalidArgumentException */ public function testCannotResolveWithPromise() { new FulfilledPromise(new Promise()); } public function testReturnsSelfWhenNoOnFulfilled() { $p = new FulfilledPromise('a'); $this->assertSame($p, $p->then()); } public function testAsynchronouslyInvokesOnFulfilled() { $p = new FulfilledPromise('a'); $r = null; $f = function ($d) use (&$r) { $r = $d; }; $p2 = $p->then($f); $this->assertNotSame($p, $p2); $this->assertNull($r); \GuzzleHttp\Promise\queue()->run(); $this->assertEquals('a', $r); } public function testReturnsNewRejectedWhenOnFulfilledFails() { $p = new FulfilledPromise('a'); $f = function () { throw new \Exception('b'); }; $p2 = $p->then($f); $this->assertNotSame($p, $p2); try { $p2->wait(); $this->fail(); } catch (\Exception $e) { $this->assertEquals('b', $e->getMessage()); } } public function testOtherwiseIsSugarForRejections() { $c = null; $p = new FulfilledPromise('foo'); $p->otherwise(function ($v) use (&$c) { $c = $v; }); $this->assertNull($c); } public function testDoesNotTryToFulfillTwiceDuringTrampoline() { $fp = new FulfilledPromise('a'); $t1 = $fp->then(function ($v) { return $v . ' b'; }); $t1->resolve('why!'); $this->assertEquals('why!', $t1->wait()); } } promises-1.0.3/tests/NotPromiseInstance.php000066400000000000000000000017011261002416000210020ustar00rootroot00000000000000nextPromise = new Promise(); } public function then(callable $res = null, callable $rej = null) { return $this->nextPromise->then($res, $rej); } public function otherwise(callable $onRejected) { return $this->then($onRejected); } public function resolve($value) { $this->nextPromise->resolve($value); } public function reject($reason) { $this->nextPromise->reject($reason); } public function wait($unwrap = true, $defaultResolution = null) { } public function cancel() { } public function getState() { return $this->nextPromise->getState(); } } promises-1.0.3/tests/PromiseTest.php000066400000000000000000000440611261002416000175020ustar00rootroot00000000000000resolve('foo'); $p->resolve('bar'); $this->assertEquals('foo', $p->wait()); } public function testCanResolveWithSameValue() { $p = new Promise(); $p->resolve('foo'); $p->resolve('foo'); } /** * @expectedException \LogicException * @expectedExceptionMessage Cannot change a fulfilled promise to rejected */ public function testCannotRejectNonPendingPromise() { $p = new Promise(); $p->resolve('foo'); $p->reject('bar'); $this->assertEquals('foo', $p->wait()); } public function testCanRejectWithSameValue() { $p = new Promise(); $p->reject('foo'); $p->reject('foo'); } /** * @expectedException \LogicException * @expectedExceptionMessage Cannot change a fulfilled promise to rejected */ public function testCannotRejectResolveWithSameValue() { $p = new Promise(); $p->resolve('foo'); $p->reject('foo'); } public function testInvokesWaitFunction() { $p = new Promise(function () use (&$p) { $p->resolve('10'); }); $this->assertEquals('10', $p->wait()); } /** * @expectedException \GuzzleHttp\Promise\RejectionException */ public function testRejectsAndThrowsWhenWaitFailsToResolve() { $p = new Promise(function () {}); $p->wait(); } /** * @expectedException \GuzzleHttp\Promise\RejectionException * @expectedExceptionMessage The promise was rejected with reason: foo */ public function testThrowsWhenUnwrapIsRejectedWithNonException() { $p = new Promise(function () use (&$p) { $p->reject('foo'); }); $p->wait(); } /** * @expectedException \UnexpectedValueException * @expectedExceptionMessage foo */ public function testThrowsWhenUnwrapIsRejectedWithException() { $e = new \UnexpectedValueException('foo'); $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); $p->wait(); } public function testDoesNotUnwrapExceptionsWhenDisabled() { $p = new Promise(function () use (&$p) { $p->reject('foo'); }); $this->assertEquals('pending', $p->getState()); $p->wait(false); $this->assertEquals('rejected', $p->getState()); } public function testRejectsSelfWhenWaitThrows() { $e = new \UnexpectedValueException('foo'); $p = new Promise(function () use ($e) { throw $e; }); try { $p->wait(); $this->fail(); } catch (\UnexpectedValueException $e) { $this->assertEquals('rejected', $p->getState()); } } public function testWaitsOnNestedPromises() { $p = new Promise(function () use (&$p) { $p->resolve('_'); }); $p2 = new Promise(function () use (&$p2) { $p2->resolve('foo'); }); $p3 = $p->then(function () use ($p2) { return $p2; }); $this->assertSame('foo', $p3->wait()); } /** * @expectedException \GuzzleHttp\Promise\RejectionException */ public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() { $p = new Promise(); $p->wait(); } public function testThrowsWaitExceptionAfterPromiseIsResolved() { $p = new Promise(function () use (&$p) { $p->reject('Foo!'); throw new \Exception('Bar?'); }); try { $p->wait(); $this->fail(); } catch (\Exception $e) { $this->assertEquals('Bar?', $e->getMessage()); } } public function testGetsActualWaitValueFromThen() { $p = new Promise(function () use (&$p) { $p->reject('Foo!'); }); $p2 = $p->then(null, function ($reason) { return new RejectedPromise([$reason]); }); try { $p2->wait(); $this->fail('Should have thrown'); } catch (RejectionException $e) { $this->assertEquals(['Foo!'], $e->getReason()); } } public function testWaitBehaviorIsBasedOnLastPromiseInChain() { $p3 = new Promise(function () use (&$p3) { $p3->resolve('Whoop'); }); $p2 = new Promise(function () use (&$p2, $p3) { $p2->reject($p3); }); $p = new Promise(function () use (&$p, $p2) { $p->reject($p2); }); $this->assertEquals('Whoop', $p->wait()); } public function testCannotCancelNonPending() { $p = new Promise(); $p->resolve('foo'); $p->cancel(); $this->assertEquals('fulfilled', $p->getState()); } /** * @expectedException \GuzzleHttp\Promise\CancellationException */ public function testCancelsPromiseWhenNoCancelFunction() { $p = new Promise(); $p->cancel(); $this->assertEquals('rejected', $p->getState()); $p->wait(); } public function testCancelsPromiseWithCancelFunction() { $called = false; $p = new Promise(null, function () use (&$called) { $called = true; }); $p->cancel(); $this->assertEquals('rejected', $p->getState()); $this->assertTrue($called); } public function testCancelsUppermostPendingPromise() { $called = false; $p1 = new Promise(null, function () use (&$called) { $called = true; }); $p2 = $p1->then(function () {}); $p3 = $p2->then(function () {}); $p4 = $p3->then(function () {}); $p3->cancel(); $this->assertEquals('rejected', $p1->getState()); $this->assertEquals('rejected', $p2->getState()); $this->assertEquals('rejected', $p3->getState()); $this->assertEquals('pending', $p4->getState()); $this->assertTrue($called); try { $p3->wait(); $this->fail(); } catch (CancellationException $e) { $this->assertContains('cancelled', $e->getMessage()); } try { $p4->wait(); $this->fail(); } catch (CancellationException $e) { $this->assertContains('cancelled', $e->getMessage()); } $this->assertEquals('rejected', $p4->getState()); } public function testCancelsChildPromises() { $called1 = $called2 = $called3 = false; $p1 = new Promise(null, function () use (&$called1) { $called1 = true; }); $p2 = new Promise(null, function () use (&$called2) { $called2 = true; }); $p3 = new Promise(null, function () use (&$called3) { $called3 = true; }); $p4 = $p2->then(function () use ($p3) { return $p3; }); $p5 = $p4->then(function () { $this->fail(); }); $p4->cancel(); $this->assertEquals('pending', $p1->getState()); $this->assertEquals('rejected', $p2->getState()); $this->assertEquals('rejected', $p4->getState()); $this->assertEquals('pending', $p5->getState()); $this->assertFalse($called1); $this->assertTrue($called2); $this->assertFalse($called3); } public function testRejectsPromiseWhenCancelFails() { $called = false; $p = new Promise(null, function () use (&$called) { $called = true; throw new \Exception('e'); }); $p->cancel(); $this->assertEquals('rejected', $p->getState()); $this->assertTrue($called); try { $p->wait(); $this->fail(); } catch (\Exception $e) { $this->assertEquals('e', $e->getMessage()); } } public function testCreatesPromiseWhenFulfilledAfterThen() { $p = new Promise(); $carry = null; $p2 = $p->then(function ($v) use (&$carry) { $carry = $v; }); $this->assertNotSame($p, $p2); $p->resolve('foo'); P\queue()->run(); $this->assertEquals('foo', $carry); } public function testCreatesPromiseWhenFulfilledBeforeThen() { $p = new Promise(); $p->resolve('foo'); $carry = null; $p2 = $p->then(function ($v) use (&$carry) { $carry = $v; }); $this->assertNotSame($p, $p2); $this->assertNull($carry); \GuzzleHttp\Promise\queue()->run(); $this->assertEquals('foo', $carry); } public function testCreatesPromiseWhenFulfilledWithNoCallback() { $p = new Promise(); $p->resolve('foo'); $p2 = $p->then(); $this->assertNotSame($p, $p2); $this->assertInstanceOf('GuzzleHttp\Promise\FulfilledPromise', $p2); } public function testCreatesPromiseWhenRejectedAfterThen() { $p = new Promise(); $carry = null; $p2 = $p->then(null, function ($v) use (&$carry) { $carry = $v; }); $this->assertNotSame($p, $p2); $p->reject('foo'); P\queue()->run(); $this->assertEquals('foo', $carry); } public function testCreatesPromiseWhenRejectedBeforeThen() { $p = new Promise(); $p->reject('foo'); $carry = null; $p2 = $p->then(null, function ($v) use (&$carry) { $carry = $v; }); $this->assertNotSame($p, $p2); $this->assertNull($carry); P\queue()->run(); $this->assertEquals('foo', $carry); } public function testCreatesPromiseWhenRejectedWithNoCallback() { $p = new Promise(); $p->reject('foo'); $p2 = $p->then(); $this->assertNotSame($p, $p2); $this->assertInstanceOf('GuzzleHttp\Promise\RejectedPromise', $p2); } public function testInvokesWaitFnsForThens() { $p = new Promise(function () use (&$p) { $p->resolve('a'); }); $p2 = $p ->then(function ($v) { return $v . '-1-'; }) ->then(function ($v) { return $v . '2'; }); $this->assertEquals('a-1-2', $p2->wait()); } public function testStacksThenWaitFunctions() { $p1 = new Promise(function () use (&$p1) { $p1->resolve('a'); }); $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); $p3 = new Promise(function () use (&$p3) { $p3->resolve('c'); }); $p4 = $p1 ->then(function () use ($p2) { return $p2; }) ->then(function () use ($p3) { return $p3; }); $this->assertEquals('c', $p4->wait()); } public function testForwardsFulfilledDownChainBetweenGaps() { $p = new Promise(); $r = $r2 = null; $p->then(null, null) ->then(function ($v) use (&$r) { $r = $v; return $v . '2'; }) ->then(function ($v) use (&$r2) { $r2 = $v; }); $p->resolve('foo'); P\queue()->run(); $this->assertEquals('foo', $r); $this->assertEquals('foo2', $r2); } public function testForwardsRejectedPromisesDownChainBetweenGaps() { $p = new Promise(); $r = $r2 = null; $p->then(null, null) ->then(null, function ($v) use (&$r) { $r = $v; return $v . '2'; }) ->then(function ($v) use (&$r2) { $r2 = $v; }); $p->reject('foo'); P\queue()->run(); $this->assertEquals('foo', $r); $this->assertEquals('foo2', $r2); } public function testForwardsThrownPromisesDownChainBetweenGaps() { $e = new \Exception(); $p = new Promise(); $r = $r2 = null; $p->then(null, null) ->then(null, function ($v) use (&$r, $e) { $r = $v; throw $e; }) ->then( null, function ($v) use (&$r2) { $r2 = $v; } ); $p->reject('foo'); P\queue()->run(); $this->assertEquals('foo', $r); $this->assertSame($e, $r2); } public function testForwardsReturnedRejectedPromisesDownChainBetweenGaps() { $p = new Promise(); $rejected = new RejectedPromise('bar'); $r = $r2 = null; $p->then(null, null) ->then(null, function ($v) use (&$r, $rejected) { $r = $v; return $rejected; }) ->then( null, function ($v) use (&$r2) { $r2 = $v; } ); $p->reject('foo'); P\queue()->run(); $this->assertEquals('foo', $r); $this->assertEquals('bar', $r2); try { $p->wait(); } catch (RejectionException $e) { $this->assertEquals('foo', $e->getReason()); } } public function testForwardsHandlersToNextPromise() { $p = new Promise(); $p2 = new Promise(); $resolved = null; $p ->then(function ($v) use ($p2) { return $p2; }) ->then(function ($value) use (&$resolved) { $resolved = $value; }); $p->resolve('a'); $p2->resolve('b'); P\queue()->run(); $this->assertEquals('b', $resolved); } public function testRemovesReferenceFromChildWhenParentWaitedUpon() { $r = null; $p = new Promise(function () use (&$p) { $p->resolve('a'); }); $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); $pb = $p->then( function ($v) use ($p2, &$r) { $r = $v; return $p2; }) ->then(function ($v) { return $v . '.'; }); $this->assertEquals('a', $p->wait()); $this->assertEquals('b', $p2->wait()); $this->assertEquals('b.', $pb->wait()); $this->assertEquals('a', $r); } public function testForwardsHandlersWhenFulfilledPromiseIsReturned() { $res = []; $p = new Promise(); $p2 = new Promise(); $p2->resolve('foo'); $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); // $res is A:foo $p ->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->resolve('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); P\queue()->run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); } public function testForwardsHandlersWhenRejectedPromiseIsReturned() { $res = []; $p = new Promise(); $p2 = new Promise(); $p2->reject('foo'); $p2->then(null, function ($v) use (&$res) { $res[] = 'A:' . $v; }); $p->then(null, function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) ->then(null, function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->reject('a'); $p->then(null, function ($v) use (&$res) { $res[] = 'D:' . $v; }); P\queue()->run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); } public function testDoesNotForwardRejectedPromise() { $res = []; $p = new Promise(); $p2 = new Promise(); $p2->cancel(); $p2->then(function ($v) use (&$res) { $res[] = "B:$v"; return $v; }); $p->then(function ($v) use ($p2, &$res) { $res[] = "B:$v"; return $p2; }) ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->resolve('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); P\queue()->run(); $this->assertEquals(['B:a', 'D:a'], $res); } public function testRecursivelyForwardsWhenOnlyThennable() { $res = []; $p = new Promise(); $p2 = new Thennable(); $p2->resolve('foo'); $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); $p->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->resolve('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); P\queue()->run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); } public function testRecursivelyForwardsWhenNotInstanceOfPromise() { $res = []; $p = new Promise(); $p2 = new NotPromiseInstance(); $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); $p->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->resolve('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); P\queue()->run(); $this->assertEquals(['B', 'D:a'], $res); $p2->resolve('foo'); P\queue()->run(); $this->assertEquals(['B', 'D:a', 'A:foo', 'C:foo'], $res); } /** * @expectedException \LogicException * @expectedExceptionMessage Cannot fulfill or reject a promise with itself */ public function testCannotResolveWithSelf() { $p = new Promise(); $p->resolve($p); } /** * @expectedException \LogicException * @expectedExceptionMessage Cannot fulfill or reject a promise with itself */ public function testCannotRejectWithSelf() { $p = new Promise(); $p->reject($p); } public function testDoesNotBlowStackWhenWaitingOnNestedThens() { $inner = new Promise(function () use (&$inner) { $inner->resolve(0); }); $prev = $inner; for ($i = 1; $i < 100; $i++) { $prev = $prev->then(function ($i) { return $i + 1; }); } $parent = new Promise(function () use (&$parent, $prev) { $parent->resolve($prev); }); $this->assertEquals(99, $parent->wait()); } public function testOtherwiseIsSugarForRejections() { $p = new Promise(); $p->reject('foo'); $p->otherwise(function ($v) use (&$c) { $c = $v; }); P\queue()->run(); $this->assertEquals($c, 'foo'); } } promises-1.0.3/tests/RejectedPromiseTest.php000066400000000000000000000073221261002416000211470ustar00rootroot00000000000000assertEquals('rejected', $p->getState()); try { $p->wait(true); $this->fail(); } catch (\Exception $e) { $this->assertEquals('rejected', $p->getState()); $this->assertContains('foo', $e->getMessage()); } } public function testCannotCancel() { $p = new RejectedPromise('foo'); $p->cancel(); $this->assertEquals('rejected', $p->getState()); } /** * @expectedException \LogicException * @exepctedExceptionMessage Cannot resolve a rejected promise */ public function testCannotResolve() { $p = new RejectedPromise('foo'); $p->resolve('bar'); } /** * @expectedException \LogicException * @exepctedExceptionMessage Cannot reject a rejected promise */ public function testCannotReject() { $p = new RejectedPromise('foo'); $p->reject('bar'); } public function testCanRejectWithSameValue() { $p = new RejectedPromise('foo'); $p->reject('foo'); } public function testThrowsSpecificException() { $e = new \Exception(); $p = new RejectedPromise($e); try { $p->wait(true); $this->fail(); } catch (\Exception $e2) { $this->assertSame($e, $e2); } } /** * @expectedException \InvalidArgumentException */ public function testCannotResolveWithPromise() { new RejectedPromise(new Promise()); } public function testReturnsSelfWhenNoOnReject() { $p = new RejectedPromise('a'); $this->assertSame($p, $p->then()); } public function testInvokesOnRejectedAsynchronously() { $p = new RejectedPromise('a'); $r = null; $f = function ($reason) use (&$r) { $r = $reason; }; $p->then(null, $f); $this->assertNull($r); \GuzzleHttp\Promise\queue()->run(); $this->assertEquals('a', $r); } public function testReturnsNewRejectedWhenOnRejectedFails() { $p = new RejectedPromise('a'); $f = function () { throw new \Exception('b'); }; $p2 = $p->then(null, $f); $this->assertNotSame($p, $p2); try { $p2->wait(); $this->fail(); } catch (\Exception $e) { $this->assertEquals('b', $e->getMessage()); } } public function testWaitingIsNoOp() { $p = new RejectedPromise('a'); $p->wait(false); } public function testOtherwiseIsSugarForRejections() { $p = new RejectedPromise('foo'); $p->otherwise(function ($v) use (&$c) { $c = $v; }); \GuzzleHttp\Promise\queue()->run(); $this->assertSame('foo', $c); } public function testCanResolveThenWithSuccess() { $actual = null; $p = new RejectedPromise('foo'); $p->otherwise(function ($v) { return $v . ' bar'; })->then(function ($v) use (&$actual) { $actual = $v; }); \GuzzleHttp\Promise\queue()->run(); $this->assertEquals('foo bar', $actual); } public function testDoesNotTryToRejectTwiceDuringTrampoline() { $fp = new RejectedPromise('a'); $t1 = $fp->then(null, function ($v) { return $v . ' b'; }); $t1->resolve('why!'); $this->assertEquals('why!', $t1->wait()); } } promises-1.0.3/tests/RejectionExceptionTest.php000066400000000000000000000017531261002416000216660ustar00rootroot00000000000000message = $message; } public function __toString() { return $this->message; } } class Thing2 implements \JsonSerializable { public function jsonSerialize() { return '{}'; } } /** * @covers GuzzleHttp\Promise\RejectionException */ class RejectionExceptionTest extends \PHPUnit_Framework_TestCase { public function testCanGetReasonFromException() { $thing = new Thing1('foo'); $e = new RejectionException($thing); $this->assertSame($thing, $e->getReason()); $this->assertEquals('The promise was rejected with reason: foo', $e->getMessage()); } public function testCanGetReasonMessageFromJson() { $reason = new Thing2(); $e = new RejectionException($reason); $this->assertContains("{}", $e->getMessage()); } } promises-1.0.3/tests/TaskQueueTest.php000066400000000000000000000014721261002416000177720ustar00rootroot00000000000000assertTrue($tq->isEmpty()); } public function testKnowsIfFull() { $tq = new TaskQueue(false); $tq->add(function () {}); $this->assertFalse($tq->isEmpty()); } public function testExecutesTasksInOrder() { $tq = new TaskQueue(false); $called = []; $tq->add(function () use (&$called) { $called[] = 'a'; }); $tq->add(function () use (&$called) { $called[] = 'b'; }); $tq->add(function () use (&$called) { $called[] = 'c'; }); $tq->run(); $this->assertEquals(['a', 'b', 'c'], $called); } } promises-1.0.3/tests/Thennable.php000066400000000000000000000007011261002416000171150ustar00rootroot00000000000000nextPromise = new Promise(); } public function then(callable $res = null, callable $rej = null) { return $this->nextPromise->then($res, $rej); } public function resolve($value) { $this->nextPromise->resolve($value); } } promises-1.0.3/tests/bootstrap.php000066400000000000000000000002041261002416000172300ustar00rootroot00000000000000assertInstanceOf('GuzzleHttp\Promise\FulfilledPromise', $p); } public function testReturnsPromiseForPromise() { $p = new Promise(); $this->assertSame($p, \GuzzleHttp\Promise\promise_for($p)); } public function testReturnsPromiseForThennable() { $p = new Thennable(); $wrapped = \GuzzleHttp\Promise\promise_for($p); $this->assertNotSame($p, $wrapped); $this->assertInstanceOf('GuzzleHttp\Promise\PromiseInterface', $wrapped); $p->resolve('foo'); P\queue()->run(); $this->assertEquals('foo', $wrapped->wait()); } public function testReturnsRejection() { $p = \GuzzleHttp\Promise\rejection_for('fail'); $this->assertInstanceOf('GuzzleHttp\Promise\RejectedPromise', $p); $this->assertEquals('fail', $this->readAttribute($p, 'reason')); } public function testReturnsPromisesAsIsInRejectionFor() { $a = new Promise(); $b = \GuzzleHttp\Promise\rejection_for($a); $this->assertSame($a, $b); } public function testWaitsOnAllPromisesIntoArray() { $e = new \Exception(); $a = new Promise(function () use (&$a) { $a->resolve('a'); }); $b = new Promise(function () use (&$b) { $b->reject('b'); }); $c = new Promise(function () use (&$c, $e) { $c->reject($e); }); $results = \GuzzleHttp\Promise\inspect_all([$a, $b, $c]); $this->assertEquals([ ['state' => 'fulfilled', 'value' => 'a'], ['state' => 'rejected', 'reason' => 'b'], ['state' => 'rejected', 'reason' => $e] ], $results); } /** * @expectedException \GuzzleHttp\Promise\RejectionException */ public function testUnwrapsPromisesWithNoDefaultAndFailure() { $promises = [new FulfilledPromise('a'), new Promise()]; \GuzzleHttp\Promise\unwrap($promises); } public function testUnwrapsPromisesWithNoDefault() { $promises = [new FulfilledPromise('a')]; $this->assertEquals(['a'], \GuzzleHttp\Promise\unwrap($promises)); } public function testUnwrapsPromisesWithKeys() { $promises = [ 'foo' => new FulfilledPromise('a'), 'bar' => new FulfilledPromise('b'), ]; $this->assertEquals([ 'foo' => 'a', 'bar' => 'b' ], \GuzzleHttp\Promise\unwrap($promises)); } public function testAllAggregatesSortedArray() { $a = new Promise(); $b = new Promise(); $c = new Promise(); $d = \GuzzleHttp\Promise\all([$a, $b, $c]); $b->resolve('b'); $a->resolve('a'); $c->resolve('c'); $d->then( function ($value) use (&$result) { $result = $value; }, function ($reason) use (&$result) { $result = $reason; } ); P\queue()->run(); $this->assertEquals(['a', 'b', 'c'], $result); } public function testAllThrowsWhenAnyRejected() { $a = new Promise(); $b = new Promise(); $c = new Promise(); $d = \GuzzleHttp\Promise\all([$a, $b, $c]); $b->resolve('b'); $a->reject('fail'); $c->resolve('c'); $d->then( function ($value) use (&$result) { $result = $value; }, function ($reason) use (&$result) { $result = $reason; } ); P\queue()->run(); $this->assertEquals('fail', $result); } public function testSomeAggregatesSortedArrayWithMax() { $a = new Promise(); $b = new Promise(); $c = new Promise(); $d = \GuzzleHttp\Promise\some(2, [$a, $b, $c]); $b->resolve('b'); $c->resolve('c'); $a->resolve('a'); $d->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals(['b', 'c'], $result); } public function testSomeRejectsWhenTooManyRejections() { $a = new Promise(); $b = new Promise(); $d = \GuzzleHttp\Promise\some(2, [$a, $b]); $a->reject('bad'); $b->resolve('good'); P\queue()->run(); $this->assertEquals($a::REJECTED, $d->getState()); $d->then(null, function ($reason) use (&$called) { $called = $reason; }); P\queue()->run(); $this->assertInstanceOf('GuzzleHttp\Promise\AggregateException', $called); $this->assertContains('bad', $called->getReason()); } public function testCanWaitUntilSomeCountIsSatisfied() { $a = new Promise(function () use (&$a) { $a->resolve('a'); }); $b = new Promise(function () use (&$b) { $b->resolve('b'); }); $c = new Promise(function () use (&$c) { $c->resolve('c'); }); $d = \GuzzleHttp\Promise\some(2, [$a, $b, $c]); $this->assertEquals(['a', 'b'], $d->wait()); } /** * @expectedException \GuzzleHttp\Promise\AggregateException * @expectedExceptionMessage Not enough promises to fulfill count */ public function testThrowsIfImpossibleToWaitForSomeCount() { $a = new Promise(function () use (&$a) { $a->resolve('a'); }); $d = \GuzzleHttp\Promise\some(2, [$a]); $d->wait(); } /** * @expectedException \GuzzleHttp\Promise\AggregateException * @expectedExceptionMessage Not enough promises to fulfill count */ public function testThrowsIfResolvedWithoutCountTotalResults() { $a = new Promise(); $b = new Promise(); $d = \GuzzleHttp\Promise\some(3, [$a, $b]); $a->resolve('a'); $b->resolve('b'); $d->wait(); } public function testAnyReturnsFirstMatch() { $a = new Promise(); $b = new Promise(); $c = \GuzzleHttp\Promise\any([$a, $b]); $b->resolve('b'); $a->resolve('a'); //P\queue()->run(); //$this->assertEquals('fulfilled', $c->getState()); $c->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals('b', $result); } public function testSettleFulfillsWithFulfilledAndRejected() { $a = new Promise(); $b = new Promise(); $c = new Promise(); $d = \GuzzleHttp\Promise\settle([$a, $b, $c]); $b->resolve('b'); $c->resolve('c'); $a->reject('a'); P\queue()->run(); $this->assertEquals('fulfilled', $d->getState()); $d->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals([ ['state' => 'rejected', 'reason' => 'a'], ['state' => 'fulfilled', 'value' => 'b'], ['state' => 'fulfilled', 'value' => 'c'] ], $result); } public function testCanInspectFulfilledPromise() { $p = new FulfilledPromise('foo'); $this->assertEquals([ 'state' => 'fulfilled', 'value' => 'foo' ], \GuzzleHttp\Promise\inspect($p)); } public function testCanInspectRejectedPromise() { $p = new RejectedPromise('foo'); $this->assertEquals([ 'state' => 'rejected', 'reason' => 'foo' ], \GuzzleHttp\Promise\inspect($p)); } public function testCanInspectRejectedPromiseWithNormalException() { $e = new \Exception('foo'); $p = new RejectedPromise($e); $this->assertEquals([ 'state' => 'rejected', 'reason' => $e ], \GuzzleHttp\Promise\inspect($p)); } public function testCallsEachLimit() { $p = new Promise(); $aggregate = \GuzzleHttp\Promise\each_limit($p, 2); $p->resolve('a'); P\queue()->run(); $this->assertEquals($p::FULFILLED, $aggregate->getState()); } public function testEachLimitAllRejectsOnFailure() { $p = [new FulfilledPromise('a'), new RejectedPromise('b')]; $aggregate = \GuzzleHttp\Promise\each_limit_all($p, 2); P\queue()->run(); $this->assertEquals(P\PromiseInterface::REJECTED, $aggregate->getState()); $result = \GuzzleHttp\Promise\inspect($aggregate); $this->assertEquals('b', $result['reason']); } public function testIterForReturnsIterator() { $iter = new \ArrayIterator(); $this->assertSame($iter, \GuzzleHttp\Promise\iter_for($iter)); } public function testKnowsIfFulfilled() { $p = new FulfilledPromise(null); $this->assertTrue(P\is_fulfilled($p)); $this->assertFalse(P\is_rejected($p)); } public function testKnowsIfRejected() { $p = new RejectedPromise(null); $this->assertTrue(P\is_rejected($p)); $this->assertFalse(P\is_fulfilled($p)); } public function testKnowsIfSettled() { $p = new RejectedPromise(null); $this->assertTrue(P\is_settled($p)); $p = new Promise(); $this->assertFalse(P\is_settled($p)); } public function testReturnsTrampoline() { $this->assertInstanceOf('GuzzleHttp\Promise\TaskQueue', P\queue()); $this->assertSame(P\queue(), P\queue()); } public function testCanScheduleThunk() { $tramp = P\queue(); $promise = P\task(function () { return 'Hi!'; }); $c = null; $promise->then(function ($v) use (&$c) { $c = $v; }); $this->assertNull($c); $tramp->run(); $this->assertEquals('Hi!', $c); } public function testCanScheduleThunkWithRejection() { $tramp = P\queue(); $promise = P\task(function () { throw new \Exception('Hi!'); }); $c = null; $promise->otherwise(function ($v) use (&$c) { $c = $v; }); $this->assertNull($c); $tramp->run(); $this->assertEquals('Hi!', $c->getMessage()); } public function testCanScheduleThunkWithWait() { $tramp = P\queue(); $promise = P\task(function () { return 'a'; }); $this->assertEquals('a', $promise->wait()); $tramp->run(); } public function testYieldsFromCoroutine() { $promise = P\coroutine(function () { $value = (yield new P\FulfilledPromise('a')); yield $value . 'b'; }); $promise->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals('ab', $result); } public function testCanCatchExceptionsInCoroutine() { $promise = P\coroutine(function () { try { yield new P\RejectedPromise('a'); $this->fail('Should have thrown into the coroutine!'); } catch (P\RejectionException $e) { $value = (yield new P\FulfilledPromise($e->getReason())); yield $value . 'b'; } }); $promise->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals(P\PromiseInterface::FULFILLED, $promise->getState()); $this->assertEquals('ab', $result); } public function testRejectsParentExceptionWhenException() { $promise = P\coroutine(function () { yield new P\FulfilledPromise(0); throw new \Exception('a'); }); $promise->then( function () { $this->fail(); }, function ($reason) use (&$result) { $result = $reason; } ); P\queue()->run(); $this->assertInstanceOf('Exception', $result); $this->assertEquals('a', $result->getMessage()); } public function testCanRejectFromRejectionCallback() { $promise = P\coroutine(function () { yield new P\FulfilledPromise(0); yield new P\RejectedPromise('no!'); }); $promise->then( function () { $this->fail(); }, function ($reason) use (&$result) { $result = $reason; } ); P\queue()->run(); $this->assertInstanceOf('GuzzleHttp\Promise\RejectionException', $result); $this->assertEquals('no!', $result->getReason()); } public function testCanAsyncReject() { $rej = new P\Promise(); $promise = P\coroutine(function () use ($rej) { yield new P\FulfilledPromise(0); yield $rej; }); $promise->then( function () { $this->fail(); }, function ($reason) use (&$result) { $result = $reason; } ); $rej->reject('no!'); P\queue()->run(); $this->assertInstanceOf('GuzzleHttp\Promise\RejectionException', $result); $this->assertEquals('no!', $result->getReason()); } public function testCanCatchAndThrowOtherException() { $promise = P\coroutine(function () { try { yield new P\RejectedPromise('a'); $this->fail('Should have thrown into the coroutine!'); } catch (P\RejectionException $e) { throw new \Exception('foo'); } }); $promise->otherwise(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals(P\PromiseInterface::REJECTED, $promise->getState()); $this->assertContains('foo', $result->getMessage()); } public function testCanCatchAndYieldOtherException() { $promise = P\coroutine(function () { try { yield new P\RejectedPromise('a'); $this->fail('Should have thrown into the coroutine!'); } catch (P\RejectionException $e) { yield new P\RejectedPromise('foo'); } }); $promise->otherwise(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals(P\PromiseInterface::REJECTED, $promise->getState()); $this->assertContains('foo', $result->getMessage()); } public function createLotsOfSynchronousPromise() { return P\coroutine(function () { $value = 0; for ($i = 0; $i < 1000; $i++) { $value = (yield new P\FulfilledPromise($i)); } yield $value; }); } public function testLotsOfSynchronousDoesNotBlowStack() { $promise = $this->createLotsOfSynchronousPromise(); $promise->then(function ($v) use (&$r) { $r = $v; }); P\queue()->run(); $this->assertEquals(999, $r); } public function testLotsOfSynchronousWaitDoesNotBlowStack() { $promise = $this->createLotsOfSynchronousPromise(); $promise->then(function ($v) use (&$r) { $r = $v; }); $this->assertEquals(999, $promise->wait()); $this->assertEquals(999, $r); } private function createLotsOfFlappingPromise() { return P\coroutine(function () { $value = 0; for ($i = 0; $i < 1000; $i++) { try { if ($i % 2) { $value = (yield new P\FulfilledPromise($i)); } else { $value = (yield new P\RejectedPromise($i)); } } catch (\Exception $e) { $value = (yield new P\FulfilledPromise($i)); } } yield $value; }); } public function testLotsOfTryCatchingDoesNotBlowStack() { $promise = $this->createLotsOfFlappingPromise(); $promise->then(function ($v) use (&$r) { $r = $v; }); P\queue()->run(); $this->assertEquals(999, $r); } public function testLotsOfTryCatchingWaitingDoesNotBlowStack() { $promise = $this->createLotsOfFlappingPromise(); $promise->then(function ($v) use (&$r) { $r = $v; }); $this->assertEquals(999, $promise->wait()); $this->assertEquals(999, $r); } public function testAsyncPromisesWithCorrectlyYieldedValues() { $promises = [ new P\Promise(), new P\Promise(), new P\Promise() ]; $promise = P\coroutine(function () use ($promises) { $value = null; $this->assertEquals('skip', (yield new P\FulfilledPromise('skip'))); foreach ($promises as $idx => $p) { $value = (yield $p); $this->assertEquals($value, $idx); $this->assertEquals('skip', (yield new P\FulfilledPromise('skip'))); } $this->assertEquals('skip', (yield new P\FulfilledPromise('skip'))); yield $value; }); $promises[0]->resolve(0); $promises[1]->resolve(1); $promises[2]->resolve(2); $promise->then(function ($v) use (&$r) { $r = $v; }); P\queue()->run(); $this->assertEquals(2, $r); } public function testYieldFinalWaitablePromise() { $p1 = new P\Promise(function () use (&$p1) { $p1->resolve('skip me'); }); $p2 = new P\Promise(function () use (&$p2) { $p2->resolve('hello!'); }); $co = P\coroutine(function() use ($p1, $p2) { yield $p1; yield $p2; }); P\queue()->run(); $this->assertEquals('hello!', $co->wait()); } public function testCanYieldFinalPendingPromise() { $p1 = new P\Promise(); $p2 = new P\Promise(); $co = P\coroutine(function() use ($p1, $p2) { yield $p1; yield $p2; }); $p1->resolve('a'); $p2->resolve('b'); $co->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals('b', $result); } public function testCanNestYieldsAndFailures() { $p1 = new P\Promise(); $p2 = new P\Promise(); $p3 = new P\Promise(); $p4 = new P\Promise(); $p5 = new P\Promise(); $co = P\coroutine(function() use ($p1, $p2, $p3, $p4, $p5) { try { yield $p1; } catch (\Exception $e) { yield $p2; try { yield $p3; yield $p4; } catch (\Exception $e) { yield $p5; } } }); $p1->reject('a'); $p2->resolve('b'); $p3->resolve('c'); $p4->reject('d'); $p5->resolve('e'); $co->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals('e', $result); } public function testCanYieldErrorsAndSuccessesWithoutRecursion() { $promises = []; for ($i = 0; $i < 20; $i++) { $promises[] = new P\Promise(); } $co = P\coroutine(function() use ($promises) { for ($i = 0; $i < 20; $i += 4) { try { yield $promises[$i]; yield $promises[$i + 1]; } catch (\Exception $e) { yield $promises[$i + 2]; yield $promises[$i + 3]; } } }); for ($i = 0; $i < 20; $i += 4) { $promises[$i]->resolve($i); $promises[$i + 1]->reject($i + 1); $promises[$i + 2]->resolve($i + 2); $promises[$i + 3]->resolve($i + 3); } $co->then(function ($value) use (&$result) { $result = $value; }); P\queue()->run(); $this->assertEquals('19', $result); } public function testCanWaitOnPromiseAfterFulfilled() { $f = function () { static $i = 0; $i++; return $p = new P\Promise(function () use (&$p, $i) { $p->resolve($i . '-bar'); }); }; $promises = []; for ($i = 0; $i < 20; $i++) { $promises[] = $f(); } $p = P\coroutine(function () use ($promises) { yield new P\FulfilledPromise('foo!'); foreach ($promises as $promise) { yield $promise; } }); $this->assertEquals('20-bar', $p->wait()); } public function testCanWaitOnErroredPromises() { $p1 = new P\Promise(function () use (&$p1) { $p1->reject('a'); }); $p2 = new P\Promise(function () use (&$p2) { $p2->resolve('b'); }); $p3 = new P\Promise(function () use (&$p3) { $p3->resolve('c'); }); $p4 = new P\Promise(function () use (&$p4) { $p4->reject('d'); }); $p5 = new P\Promise(function () use (&$p5) { $p5->resolve('e'); }); $p6 = new P\Promise(function () use (&$p6) { $p6->reject('f'); }); $co = P\coroutine(function() use ($p1, $p2, $p3, $p4, $p5, $p6) { try { yield $p1; } catch (\Exception $e) { yield $p2; try { yield $p3; yield $p4; } catch (\Exception $e) { yield $p5; yield $p6; } } }); $res = P\inspect($co); $this->assertEquals('f', $res['reason']); } public function testCoroutineOtherwiseIntegrationTest() { $a = new P\Promise(); $b = new P\Promise(); $promise = P\coroutine(function () use ($a, $b) { // Execute the pool of commands concurrently, and process errors. yield $a; yield $b; })->otherwise(function (\Exception $e) { // Throw errors from the operations as a specific Multipart error. throw new \OutOfBoundsException('a', 0, $e); }); $a->resolve('a'); $b->reject('b'); $reason = P\inspect($promise)['reason']; $this->assertInstanceOf('OutOfBoundsException', $reason); $this->assertInstanceOf('GuzzleHttp\Promise\RejectionException', $reason->getPrevious()); } }