From 0b02a6d123b2014705998c94ddf3d460948d3eac Mon Sep 17 00:00:00 2001 From: redmatrix Date: Tue, 10 May 2016 17:26:44 -0700 Subject: initial sabre upgrade (needs lots of work - to wit: authentication, redo the browser interface, and rework event export/import) --- vendor/sabre/event/lib/EventEmitter.php | 18 + vendor/sabre/event/lib/EventEmitterInterface.php | 100 ++++++ vendor/sabre/event/lib/EventEmitterTrait.php | 211 +++++++++++ vendor/sabre/event/lib/Loop/Loop.php | 386 +++++++++++++++++++++ vendor/sabre/event/lib/Loop/functions.php | 183 ++++++++++ vendor/sabre/event/lib/Promise.php | 320 +++++++++++++++++ vendor/sabre/event/lib/Promise/functions.php | 135 +++++++ .../event/lib/PromiseAlreadyResolvedException.php | 15 + vendor/sabre/event/lib/Version.php | 19 + vendor/sabre/event/lib/coroutine.php | 120 +++++++ 10 files changed, 1507 insertions(+) create mode 100644 vendor/sabre/event/lib/EventEmitter.php create mode 100644 vendor/sabre/event/lib/EventEmitterInterface.php create mode 100644 vendor/sabre/event/lib/EventEmitterTrait.php create mode 100644 vendor/sabre/event/lib/Loop/Loop.php create mode 100644 vendor/sabre/event/lib/Loop/functions.php create mode 100644 vendor/sabre/event/lib/Promise.php create mode 100644 vendor/sabre/event/lib/Promise/functions.php create mode 100644 vendor/sabre/event/lib/PromiseAlreadyResolvedException.php create mode 100644 vendor/sabre/event/lib/Version.php create mode 100644 vendor/sabre/event/lib/coroutine.php (limited to 'vendor/sabre/event/lib') diff --git a/vendor/sabre/event/lib/EventEmitter.php b/vendor/sabre/event/lib/EventEmitter.php new file mode 100644 index 000000000..1bb1c3cf9 --- /dev/null +++ b/vendor/sabre/event/lib/EventEmitter.php @@ -0,0 +1,18 @@ +listeners[$eventName])) { + $this->listeners[$eventName] = [ + true, // If there's only one item, it's sorted + [$priority], + [$callBack] + ]; + } else { + $this->listeners[$eventName][0] = false; // marked as unsorted + $this->listeners[$eventName][1][] = $priority; + $this->listeners[$eventName][2][] = $callBack; + } + + } + + /** + * Subscribe to an event exactly once. + * + * @param string $eventName + * @param callable $callBack + * @param int $priority + * @return void + */ + function once($eventName, callable $callBack, $priority = 100) { + + $wrapper = null; + $wrapper = function() use ($eventName, $callBack, &$wrapper) { + + $this->removeListener($eventName, $wrapper); + return call_user_func_array($callBack, func_get_args()); + + }; + + $this->on($eventName, $wrapper, $priority); + + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were succesfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + * + * @param string $eventName + * @param array $arguments + * @param callback $continueCallBack + * @return bool + */ + function emit($eventName, array $arguments = [], callable $continueCallBack = null) { + + if (is_null($continueCallBack)) { + + foreach ($this->listeners($eventName) as $listener) { + + $result = call_user_func_array($listener, $arguments); + if ($result === false) { + return false; + } + } + + } else { + + $listeners = $this->listeners($eventName); + $counter = count($listeners); + + foreach ($listeners as $listener) { + + $counter--; + $result = call_user_func_array($listener, $arguments); + if ($result === false) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) break; + } + + } + + } + + return true; + + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @param string $eventName + * @return callable[] + */ + function listeners($eventName) { + + if (!isset($this->listeners[$eventName])) { + return []; + } + + // The list is not sorted + if (!$this->listeners[$eventName][0]) { + + // Sorting + array_multisort($this->listeners[$eventName][1], SORT_NUMERIC, $this->listeners[$eventName][2]); + + // Marking the listeners as sorted + $this->listeners[$eventName][0] = true; + } + + return $this->listeners[$eventName][2]; + + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + * + * @param string $eventName + * @param callable $listener + * @return bool + */ + function removeListener($eventName, callable $listener) { + + if (!isset($this->listeners[$eventName])) { + return false; + } + foreach ($this->listeners[$eventName][2] as $index => $check) { + if ($check === $listener) { + unset($this->listeners[$eventName][1][$index]); + unset($this->listeners[$eventName][2][$index]); + return true; + } + } + return false; + + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + * + * @param string $eventName + * @return void + */ + function removeAllListeners($eventName = null) { + + if (!is_null($eventName)) { + unset($this->listeners[$eventName]); + } else { + $this->listeners = []; + } + + } + +} diff --git a/vendor/sabre/event/lib/Loop/Loop.php b/vendor/sabre/event/lib/Loop/Loop.php new file mode 100644 index 000000000..86ee7c8b0 --- /dev/null +++ b/vendor/sabre/event/lib/Loop/Loop.php @@ -0,0 +1,386 @@ +timers) { + // Special case when the timers array was empty. + $this->timers[] = [$triggerTime, $cb]; + return; + } + + // We need to insert these values in the timers array, but the timers + // array must be in reverse-order of trigger times. + // + // So here we search the array for the insertion point. + $index = count($this->timers) - 1; + while (true) { + if ($triggerTime < $this->timers[$index][0]) { + array_splice( + $this->timers, + $index + 1, + 0, + [[$triggerTime, $cb]] + ); + break; + } elseif ($index === 0) { + array_unshift($this->timers, [$triggerTime, $cb]); + break; + } + $index--; + + } + + } + + /** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + * + * @param callable $cb + * @param float $timeout + * @return array + */ + function setInterval(callable $cb, $timeout) { + + $keepGoing = true; + $f = null; + + $f = function() use ($cb, &$f, $timeout, &$keepGoing) { + if ($keepGoing) { + $cb(); + $this->setTimeout($f, $timeout); + } + }; + $this->setTimeout($f, $timeout); + + // Really the only thing that matters is returning the $keepGoing + // boolean value. + // + // We need to pack it in an array to allow returning by reference. + // Because I'm worried people will be confused by using a boolean as a + // sort of identifier, I added an extra string. + return ['I\'m an implementation detail', &$keepGoing]; + + } + + /** + * Stops a running internval. + * + * @param array $intervalId + * @return void + */ + function clearInterval($intervalId) { + + $intervalId[1] = false; + + } + + /** + * Runs a function immediately at the next iteration of the loop. + * + * @param callable $cb + * @return void + */ + function nextTick(callable $cb) { + + $this->nextTick[] = $cb; + + } + + + /** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + * @param callable $cb + * @return void + */ + function addReadStream($stream, callable $cb) { + + $this->readStreams[(int)$stream] = $stream; + $this->readCallbacks[(int)$stream] = $cb; + + } + + /** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + * @param callable $cb + * @return void + */ + function addWriteStream($stream, callable $cb) { + + $this->writeStreams[(int)$stream] = $stream; + $this->writeCallbacks[(int)$stream] = $cb; + + } + + /** + * Stop watching a stream for reads. + * + * @param resource $stream + * @return void + */ + function removeReadStream($stream) { + + unset( + $this->readStreams[(int)$stream], + $this->readCallbacks[(int)$stream] + ); + + } + + /** + * Stop watching a stream for writes. + * + * @param resource $stream + * @return void + */ + function removeWriteStream($stream) { + + unset( + $this->writeStreams[(int)$stream], + $this->writeCallbacks[(int)$stream] + ); + + } + + + /** + * Runs the loop. + * + * This function will run continiously, until there's no more events to + * handle. + * + * @return void + */ + function run() { + + $this->running = true; + + do { + + $hasEvents = $this->tick(true); + + } while ($this->running && $hasEvents); + $this->running = false; + + } + + /** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + * + * @param bool $block + * @return bool + */ + function tick($block = false) { + + $this->runNextTicks(); + $nextTimeout = $this->runTimers(); + + // Calculating how long runStreams should at most wait. + if (!$block) { + // Don't wait + $streamWait = 0; + } elseif ($this->nextTick) { + // There's a pending 'nextTick'. Don't wait. + $streamWait = 0; + } elseif (is_numeric($nextTimeout)) { + // Wait until the next Timeout should trigger. + $streamWait = $nextTimeout; + } else { + // Wait indefinitely + $streamWait = null; + } + + $this->runStreams($streamWait); + + return ($this->readStreams || $this->writeStreams || $this->nextTick || $this->timers); + + } + + /** + * Stops a running eventloop + * + * @return void + */ + function stop() { + + $this->running = false; + + } + + /** + * Executes all 'nextTick' callbacks. + * + * return void + */ + protected function runNextTicks() { + + $nextTick = $this->nextTick; + $this->nextTick = []; + + foreach ($nextTick as $cb) { + $cb(); + } + + } + + /** + * Runs all pending timers. + * + * After running the timer callbacks, this function returns the number of + * seconds until the next timer should be executed. + * + * If there's no more pending timers, this function returns null. + * + * @return float + */ + protected function runTimers() { + + $now = microtime(true); + while (($timer = array_pop($this->timers)) && $timer[0] < $now) { + $timer[1](); + } + // Add the last timer back to the array. + if ($timer) { + $this->timers[] = $timer; + return $timer[0] - microtime(true); + } + + } + + /** + * Runs all pending stream events. + * + * @param float $timeout + */ + protected function runStreams($timeout) { + + if ($this->readStreams || $this->writeStreams) { + + $read = $this->readStreams; + $write = $this->writeStreams; + $except = null; + if (stream_select($read, $write, $except, null, $timeout)) { + + // See PHP Bug https://bugs.php.net/bug.php?id=62452 + // Fixed in PHP7 + foreach ($read as $readStream) { + $readCb = $this->readCallbacks[(int)$readStream]; + $readCb(); + } + foreach ($write as $writeStream) { + $writeCb = $this->writeCallbacks[(int)$writeStream]; + $writeCb(); + } + + } + + } elseif ($this->running && ($this->nextTick || $this->timers)) { + usleep($timeout !== null ? $timeout * 1000000 : 200000); + } + + } + + /** + * Is the main loop active + * + * @var bool + */ + protected $running = false; + + /** + * A list of timers, added by setTimeout. + * + * @var array + */ + protected $timers = []; + + /** + * A list of 'nextTick' callbacks. + * + * @var callable[] + */ + protected $nextTick = []; + + /** + * List of readable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $readStreams = []; + + /** + * List of writable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $writeStreams = []; + + /** + * List of read callbacks, indexed by stream id. + * + * @var callback[] + */ + protected $readCallbacks = []; + + /** + * List of write callbacks, indexed by stream id. + * + * @var callback[] + */ + protected $writeCallbacks = []; + + +} diff --git a/vendor/sabre/event/lib/Loop/functions.php b/vendor/sabre/event/lib/Loop/functions.php new file mode 100644 index 000000000..56c5bc8c7 --- /dev/null +++ b/vendor/sabre/event/lib/Loop/functions.php @@ -0,0 +1,183 @@ +setTimeout($cb, $timeout); + +} + +/** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + * + * @param callable $cb + * @param float $timeout + * @return array + */ +function setInterval(callable $cb, $timeout) { + + return instance()->setInterval($cb, $timeout); + +} + +/** + * Stops a running internval. + * + * @param array $intervalId + * @return void + */ +function clearInterval($intervalId) { + + instance()->clearInterval($intervalId); + +} + +/** + * Runs a function immediately at the next iteration of the loop. + * + * @param callable $cb + * @return void + */ +function nextTick(callable $cb) { + + instance()->nextTick($cb); + +} + + +/** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + * @param callable $cb + * @return void + */ +function addReadStream($stream, callable $cb) { + + instance()->addReadStream($stream, $cb); + +} + +/** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + * @param callable $cb + * @return void + */ +function addWriteStream($stream, callable $cb) { + + instance()->addWriteStream($stream, $cb); + +} + +/** + * Stop watching a stream for reads. + * + * @param resource $stream + * @return void + */ +function removeReadStream($stream) { + + instance()->removeReadStream($stream); + +} + +/** + * Stop watching a stream for writes. + * + * @param resource $stream + * @return void + */ +function removeWriteStream($stream) { + + instance()->removeWriteStream($stream); + +} + + +/** + * Runs the loop. + * + * This function will run continiously, until there's no more events to + * handle. + * + * @return void + */ +function run() { + + instance()->run(); + +} + +/** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + * + * @param bool $block + * @return bool + */ +function tick($block = false) { + + return instance()->tick($block); + +} + +/** + * Stops a running eventloop + * + * @return void + */ +function stop() { + + instance()->stop(); + +} + +/** + * Retrieves or sets the global Loop object. + * + * @param Loop $newLoop + */ +function instance(Loop $newLoop = null) { + + static $loop; + if ($newLoop) { + $loop = $newLoop; + } elseif (!$loop) { + $loop = new Loop(); + } + return $loop; + +} diff --git a/vendor/sabre/event/lib/Promise.php b/vendor/sabre/event/lib/Promise.php new file mode 100644 index 000000000..1c874c1bd --- /dev/null +++ b/vendor/sabre/event/lib/Promise.php @@ -0,0 +1,320 @@ +fulfill and $this->reject. + * Using the executor is optional. + * + * @param callable $executor + */ + function __construct(callable $executor = null) { + + if ($executor) { + $executor( + [$this, 'fulfill'], + [$this, 'reject'] + ); + } + + } + + /** + * This method allows you to specify the callback that will be called after + * the promise has been fulfilled or rejected. + * + * Both arguments are optional. + * + * This method returns a new promise, which can be used for chaining. + * If either the onFulfilled or onRejected callback is called, you may + * return a result from this callback. + * + * If the result of this callback is yet another promise, the result of + * _that_ promise will be used to set the result of the returned promise. + * + * If either of the callbacks return any other value, the returned promise + * is automatically fulfilled with that value. + * + * If either of the callbacks throw an exception, the returned promise will + * be rejected and the exception will be passed back. + * + * @param callable $onFulfilled + * @param callable $onRejected + * @return Promise + */ + function then(callable $onFulfilled = null, callable $onRejected = null) { + + // This new subPromise will be returned from this function, and will + // be fulfilled with the result of the onFulfilled or onRejected event + // handlers. + $subPromise = new self(); + + switch ($this->state) { + case self::PENDING : + // The operation is pending, so we keep a reference to the + // event handlers so we can call them later. + $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; + break; + case self::FULFILLED : + // The async operation is already fulfilled, so we trigger the + // onFulfilled callback asap. + $this->invokeCallback($subPromise, $onFulfilled); + break; + case self::REJECTED : + // The async operation failed, so we call teh onRejected + // callback asap. + $this->invokeCallback($subPromise, $onRejected); + break; + } + return $subPromise; + + } + + /** + * Add a callback for when this promise is rejected. + * + * Its usage is identical to then(). However, the otherwise() function is + * preferred. + * + * @param callable $onRejected + * @return Promise + */ + function otherwise(callable $onRejected) { + + return $this->then(null, $onRejected); + + } + + /** + * Marks this promise as fulfilled and sets its return value. + * + * @param mixed $value + * @return void + */ + function fulfill($value = null) { + if ($this->state !== self::PENDING) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::FULFILLED; + $this->value = $value; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[1]); + } + } + + /** + * Marks this promise as rejected, and set it's rejection reason. + * + * While it's possible to use any PHP value as the reason, it's highly + * recommended to use an Exception for this. + * + * @param mixed $reason + * @return void + */ + function reject($reason = null) { + if ($this->state !== self::PENDING) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::REJECTED; + $this->value = $reason; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[2]); + } + + } + + /** + * Stops execution until this promise is resolved. + * + * This method stops exection completely. If the promise is successful with + * a value, this method will return this value. If the promise was + * rejected, this method will throw an exception. + * + * This effectively turns the asynchronous operation into a synchronous + * one. In PHP it might be useful to call this on the last promise in a + * chain. + * + * @throws Exception + * @return mixed + */ + function wait() { + + $hasEvents = true; + while ($this->state === self::PENDING) { + + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + } + + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = Loop\tick(true); + + } + + if ($this->state === self::FULFILLED) { + // If the state of this promise is fulfilled, we can return the value. + return $this->value; + } else { + // If we got here, it means that the asynchronous operation + // errored. Therefore we need to throw an exception. + $reason = $this->value; + if ($reason instanceof Exception) { + throw $reason; + } elseif (is_scalar($reason)) { + throw new Exception($reason); + } else { + $type = is_object($reason) ? get_class($reason) : gettype($reason); + throw new Exception('Promise was rejected with reason of type: ' . $type); + } + } + + + } + + + /** + * A list of subscribers. Subscribers are the callbacks that want us to let + * them know if the callback was fulfilled or rejected. + * + * @var array + */ + protected $subscribers = []; + + /** + * The result of the promise. + * + * If the promise was fulfilled, this will be the result value. If the + * promise was rejected, this property hold the rejection reason. + * + * @var mixed + */ + protected $value = null; + + /** + * 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); + } + } + }); + } + + /** + * Alias for 'otherwise'. + * + * This function is now deprecated and will be removed in a future version. + * + * @param callable $onRejected + * @deprecated + * @return Promise + */ + function error(callable $onRejected) { + + return $this->otherwise($onRejected); + + } + + /** + * Deprecated. + * + * Please use Sabre\Event\Promise::all + * + * @param Promise[] $promises + * @deprecated + * @return Promise + */ + static function all(array $promises) { + + return Promise\all($promises); + + } + +} diff --git a/vendor/sabre/event/lib/Promise/functions.php b/vendor/sabre/event/lib/Promise/functions.php new file mode 100644 index 000000000..3604b8aaa --- /dev/null +++ b/vendor/sabre/event/lib/Promise/functions.php @@ -0,0 +1,135 @@ + $subPromise) { + + $subPromise->then( + function($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) { + $completeResult[$promiseIndex] = $result; + $successCount++; + if ($successCount === count($promises)) { + $success($completeResult); + } + return $result; + } + )->error( + function($reason) use ($fail) { + $fail($reason); + } + ); + + } + }); + +} + +/** + * The race function returns a promise that resolves or rejects as soon as + * one of the promises in the argument resolves or rejects. + * + * The returned promise will resolve or reject with the value or reason of + * that first promise. + * + * @param Promise[] $promises + * @return Promise + */ +function race(array $promises) { + + return new Promise(function($success, $fail) use ($promises) { + + $alreadyDone = false; + foreach ($promises as $promise) { + + $promise->then( + function($result) use ($success, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $success($result); + }, + function($reason) use ($fail, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $fail($reason); + } + ); + + } + + }); + +} + + +/** + * Returns a Promise that resolves with the given value. + * + * If the value is a promise, the returned promise will attach itself to that + * promise and eventually get the same state as the followed promise. + * + * @param mixed $value + * @return Promise + */ +function resolve($value) { + + if ($value instanceof Promise) { + return $value->then(); + } else { + $promise = new Promise(); + $promise->fulfill($value); + return $promise; + } + +} + +/** + * 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; + +} diff --git a/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php b/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php new file mode 100644 index 000000000..86a6c5b3f --- /dev/null +++ b/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php @@ -0,0 +1,15 @@ +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; + +} -- cgit v1.2.3