/** * Turn asynchronous promise-based code into something that looks synchronous * again, through the use of generators. * * Example without coroutines: * * $promise = $httpClient->request('GET', '/foo'); * $promise->then(function($value) { * * return $httpClient->request('DELETE','/foo'); * * })->then(function($value) { * * return $httpClient->request('PUT', '/foo'); * * })->error(function($reason) { * * echo "Failed because: $reason\n"; * * }); * * Example with coroutines: * * coroutine(function() { * * try { * yield $httpClient->request('GET', '/foo'); * yield $httpClient->request('DELETE', /foo'); * yield $httpClient->request('PUT', '/foo'); * } catch(\Exception $reason) { * echo "Failed because: $reason\n"; * } * * }); * * @copyright Copyright (C) 2013-2015 fruux GmbH. All rights reserved. * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License */ function coroutine(callable $gen) { $generator = $gen(); if (!$generator instanceof Generator) { throw new \InvalidArgumentException('You must pass a generator function'); } // This is the value we're returning. $promise = new Promise(); $lastYieldResult = null; /** * So tempted to use the mythical y-combinator here, but it's not needed in * PHP. */ $advanceGenerator = function () use(&$advanceGenerator, $generator, $promise, &$lastYieldResult) { while ($generator->valid()) { $yieldedValue = $generator->current(); if ($yieldedValue instanceof Promise) { $yieldedValue->then(function ($value) use($generator, &$advanceGenerator, &$lastYieldResult) { $lastYieldResult = $value; $generator->send($value); $advanceGenerator(); }, function ($reason) use($generator, $advanceGenerator) { if ($reason instanceof Exception) { $generator->throw($reason); } elseif (is_scalar($reason)) { $generator->throw(new Exception($reason)); } else { $type = is_object($reason) ? get_class($reason) : gettype($reason); $generator->throw(new Exception('Promise was rejected with reason of type: ' . $type)); } $advanceGenerator(); })->error(function ($reason) use($promise) { // This error handler would be called, if something in the // generator throws an exception, and it's not caught // locally. $promise->reject($reason); }); // We need to break out of the loop, because $advanceGenerator // will be called asynchronously when the promise has a result. break; } else { // If the value was not a promise, we'll just let it pass through. $lastYieldResult = $yieldedValue; $generator->send($yieldedValue); } } // If the generator is at the end, and we didn't run into an exception, // we can fullfill the promise with the last thing that was yielded to // us. if (!$generator->valid() && $promise->state === Promise::PENDING) { $promise->fulfill($lastYieldResult); } }; try { $advanceGenerator(); } catch (Exception $e) { $promise->reject($e); } return $promise; }
/** * This method is used to call either an onFulfilled or onRejected callback. * * This method makes sure that the result of these callbacks are handled * correctly, and any chained promises are also correctly fulfilled or * rejected. * * @param Promise $subPromise * @param callable $callBack * @return void */ private function invokeCallback(Promise $subPromise, callable $callBack = null) { // We use 'nextTick' to ensure that the event handlers are always // triggered outside of the calling stack in which they were originally // passed to 'then'. // // This makes the order of execution more predictable. Loop\nextTick(function () use($callBack, $subPromise) { if (is_callable($callBack)) { try { $result = $callBack($this->value); if ($result instanceof self) { // If the callback (onRejected or onFulfilled) // returned a promise, we only fulfill or reject the // chained promise once that promise has also been // resolved. $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); } else { // If the callback returned any other value, we // immediately fulfill the chained promise. $subPromise->fulfill($result); } } catch (Exception $e) { // If the event handler threw an exception, we need to make sure that // the chained promise is rejected as well. $subPromise->reject($e); } } else { if ($this->state === self::FULFILLED) { $subPromise->fulfill($this->value); } else { $subPromise->reject($this->value); } } }); }
$promise->fulfill($value . ", how are ya?"); }, 2); return $promise; })->then(function ($value) { echo "Step 4\n"; // This is the final event handler. return $value . " you rock!"; })->wait(); echo $result, "\n"; /* Now an identical example, this time with coroutines. */ $result = coroutine(function () { $promise = new Promise(); /* After 2 seconds we fulfill it */ Loop\setTimeout(function () use($promise) { echo "Step 1\n"; $promise->fulfill("hello"); }, 2); $value = (yield $promise); echo "Step 2\n"; $value .= ' world'; echo "Step 3\n"; $promise = new Promise(); Loop\setTimeout(function () use($promise, $value) { $promise->fulfill($value . ", how are ya?"); }, 2); $value = (yield $promise); echo "Step 4\n"; // This is the final event handler. (yield $value . " you rock!"); })->wait(); echo $result, "\n";
/** * Returns a Promise that will reject with the given reason. * * @param mixed $reason * @return Promise */ function reject($reason) { $promise = new Promise(); $promise->reject($reason); return $promise; }
function testWaitRejectedNonScalar() { $promise = new Promise(); Loop\nextTick(function () use($promise) { $promise->reject([]); }); try { $promise->wait(); $this->fail('We did not get the expected exception'); } catch (\Exception $e) { $this->assertInstanceOf('Exception', $e); $this->assertEquals('Promise was rejected with reason of type: array', $e->getMessage()); } }
/** * @expectedException \Exception */ function testResolvePromise() { $finalValue = 0; $promise = new Promise(); $promise->reject(new \Exception('uh oh')); $newPromise = resolve($promise); $newPromise->wait(); }
/** * This method is used to call either an onFulfilled or onRejected callback. * * This method makes sure that the result of these callbacks are handled * correctly, and any chained promises are also correctly fulfilled or * rejected. * * @param Promise $subPromise * @param callable $callBack * @return void */ protected function invokeCallback(Promise $subPromise, callable $callBack = null) { if (is_callable($callBack)) { try { $result = $callBack($this->value); if ($result instanceof self) { $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); } else { $subPromise->fulfill($result); } } catch (Exception $e) { $subPromise->reject($e); } } else { if ($this->state === self::FULFILLED) { $subPromise->fulfill($this->value); } else { $subPromise->reject($this->value); } } }