├── bin └── .empty ├── phpstan.neon ├── .php_cs.dist ├── lib ├── CancellationException.php ├── Version.php ├── Emitter.php ├── PromiseAlreadyResolvedException.php ├── EventEmitter.php ├── WildcardEmitter.php ├── RejectionException.php ├── EmitterInterface.php ├── Loop │ ├── functions.php │ └── Loop.php ├── Promise │ └── functions.php ├── coroutine.php ├── EmitterTrait.php ├── WildcardEmitterTrait.php └── Promise.php ├── composer.json └── LICENSE /bin/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 0 3 | bootstrap: %currentWorkingDirectory%/vendor/autoload.php 4 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | getFinder() 5 | ->exclude('vendor') 6 | ->in(__DIR__); 7 | $config->setRules([ 8 | '@PSR1' => true, 9 | '@Symfony' =>true 10 | ]); 11 | 12 | return $config; -------------------------------------------------------------------------------- /lib/CancellationException.php: -------------------------------------------------------------------------------- 1 | =7.0" 20 | }, 21 | "authors": [ 22 | { 23 | "name": "Evert Pot", 24 | "email": "me@evertpot.com", 25 | "homepage": "http://evertpot.com/", 26 | "role": "Developer" 27 | } 28 | ], 29 | "support": { 30 | "forum": "https://groups.google.com/group/sabredav-discuss", 31 | "source": "https://github.com/fruux/sabre-event" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Sabre\\Event\\": "lib/" 36 | }, 37 | "files" : [ 38 | "lib/coroutine.php", 39 | "lib/Loop/functions.php", 40 | "lib/Promise/functions.php" 41 | ] 42 | }, 43 | "require-dev": { 44 | "phpunit/phpunit" : ">=6" 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Sabre\\Tests\\": "tests/" 49 | } 50 | }, 51 | "config" : { 52 | "bin-dir" : "bin/" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/RejectionException.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 22 | 23 | $message = 'The promise was rejected'; 24 | 25 | if ($description) { 26 | $message .= ' with reason: ' . $description; 27 | } elseif (is_string($reason) 28 | || (is_object($reason) && method_exists($reason, '__toString')) 29 | ) { 30 | $message .= ' with reason: ' . $this->reason; 31 | } elseif ($reason instanceof \JsonSerializable) { 32 | $message .= ' with reason: ' 33 | . json_encode($this->reason, JSON_PRETTY_PRINT); 34 | } 35 | 36 | parent::__construct($message); 37 | } 38 | 39 | /** 40 | * Returns the rejection reason. 41 | * 42 | * @return mixed 43 | */ 44 | public function getReason() 45 | { 46 | return $this->reason; 47 | } 48 | 49 | public function wait($unwrap = true) 50 | { 51 | return $unwrap ? $this->reason : null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2016 fruux GmbH (https://fruux.com/) 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name Sabre nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /lib/EmitterInterface.php: -------------------------------------------------------------------------------- 1 | setTimeout($cb, $timeout); 13 | } 14 | 15 | /** 16 | * Executes a function every x seconds. 17 | * 18 | * The value this function returns can be used to stop the interval with 19 | * clearInterval. 20 | */ 21 | function setInterval(callable $cb, float $timeout): array 22 | { 23 | return instance()->setInterval($cb, $timeout); 24 | } 25 | 26 | /** 27 | * Stops a running interval. 28 | */ 29 | function clearInterval(array $intervalId) 30 | { 31 | instance()->clearInterval($intervalId); 32 | } 33 | 34 | /** 35 | * Runs a function immediately at the next iteration of the loop. 36 | */ 37 | function nextTick(callable $cb) 38 | { 39 | instance()->nextTick($cb); 40 | } 41 | 42 | /** 43 | * Adds a read stream. 44 | * 45 | * The callback will be called as soon as there is something to read from 46 | * the stream. 47 | * 48 | * You MUST call removeReadStream after you are done with the stream, to 49 | * prevent the eventloop from never stopping. 50 | * 51 | * @param resource $stream 52 | */ 53 | function addReadStream($stream, callable $cb) 54 | { 55 | instance()->addReadStream($stream, $cb); 56 | } 57 | 58 | /** 59 | * Adds a write stream. 60 | * 61 | * The callback will be called as soon as the system reports it's ready to 62 | * receive writes on the stream. 63 | * 64 | * You MUST call removeWriteStream after you are done with the stream, to 65 | * prevent the eventloop from never stopping. 66 | * 67 | * @param resource $stream 68 | */ 69 | function addWriteStream($stream, callable $cb) 70 | { 71 | instance()->addWriteStream($stream, $cb); 72 | } 73 | 74 | /** 75 | * Stop watching a stream for reads. 76 | * 77 | * @param resource $stream 78 | */ 79 | function removeReadStream($stream) 80 | { 81 | instance()->removeReadStream($stream); 82 | } 83 | 84 | /** 85 | * Stop watching a stream for writes. 86 | * 87 | * @param resource $stream 88 | */ 89 | function removeWriteStream($stream) 90 | { 91 | instance()->removeWriteStream($stream); 92 | } 93 | 94 | /** 95 | * Runs the loop. 96 | * 97 | * This function will run continuously, until there's no more events to 98 | * handle. 99 | */ 100 | function run() 101 | { 102 | instance()->run(); 103 | } 104 | 105 | /** 106 | * Executes all pending events. 107 | * 108 | * If $block is turned true, this function will block until any event is 109 | * triggered. 110 | * 111 | * If there are now timeouts, nextTick callbacks or events in the loop at 112 | * all, this function will exit immediately. 113 | * 114 | * This function will return true if there are _any_ events left in the 115 | * loop after the tick. 116 | */ 117 | function tick(bool $block = false): bool 118 | { 119 | return instance()->tick($block); 120 | } 121 | 122 | /** 123 | * Stops a running eventloop. 124 | */ 125 | function stop() 126 | { 127 | instance()->stop(); 128 | } 129 | 130 | /** 131 | * Retrieves or sets the global Loop object. 132 | */ 133 | function instance(Loop $newLoop = null): Loop 134 | { 135 | static $loop; 136 | if ($newLoop) { 137 | $loop = $newLoop; 138 | } elseif (!$loop) { 139 | $loop = new Loop(); 140 | } 141 | 142 | return $loop; 143 | } 144 | -------------------------------------------------------------------------------- /lib/Promise/functions.php: -------------------------------------------------------------------------------- 1 | $subPromise) { 46 | $subPromise->then( 47 | function ($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) { 48 | $completeResult[$promiseIndex] = $result; 49 | ++$successCount; 50 | if ($successCount === count($promises)) { 51 | $success($completeResult); 52 | } 53 | 54 | return $result; 55 | } 56 | )->otherwise( 57 | function ($reason) use ($fail) { 58 | $fail($reason); 59 | } 60 | ); 61 | } 62 | }); 63 | } 64 | 65 | /** 66 | * The race function returns a promise that resolves or rejects as soon as 67 | * one of the promises in the argument resolves or rejects. 68 | * 69 | * The returned promise will resolve or reject with the value or reason of 70 | * that first promise. 71 | * 72 | * @param Promise[] $promises 73 | */ 74 | function race(array $promises): Promise 75 | { 76 | return new Promise(function ($success, $fail) use ($promises) { 77 | $alreadyDone = false; 78 | foreach ($promises as $promise) { 79 | $promise->then( 80 | function ($result) use ($success, &$alreadyDone) { 81 | if ($alreadyDone) { 82 | return; 83 | } 84 | $alreadyDone = true; 85 | $success($result); 86 | }, 87 | function ($reason) use ($fail, &$alreadyDone) { 88 | if ($alreadyDone) { 89 | return; 90 | } 91 | $alreadyDone = true; 92 | $fail($reason); 93 | } 94 | ); 95 | } 96 | }); 97 | } 98 | 99 | /** 100 | * Returns a Promise that resolves with the given value. 101 | * 102 | * If the value is a promise, the returned promise will attach itself to that 103 | * promise and eventually get the same state as the followed promise. 104 | * 105 | * @param mixed $value 106 | */ 107 | function resolve($value): Promise 108 | { 109 | if ($value instanceof Promise) { 110 | return $value->then(); 111 | } else { 112 | $promise = new Promise(); 113 | $promise->fulfill($value); 114 | 115 | return $promise; 116 | } 117 | } 118 | 119 | /** 120 | * Returns a Promise that will reject with the given reason. 121 | */ 122 | function reject(Throwable $reason): Promise 123 | { 124 | $promise = new Promise(); 125 | $promise->reject($reason); 126 | 127 | return $promise; 128 | } 129 | -------------------------------------------------------------------------------- /lib/coroutine.php: -------------------------------------------------------------------------------- 1 | request('GET', '/foo'); 17 | * $promise->then(function($value) { 18 | * 19 | * return $httpClient->request('DELETE','/foo'); 20 | * 21 | * })->then(function($value) { 22 | * 23 | * return $httpClient->request('PUT', '/foo'); 24 | * 25 | * })->error(function($reason) { 26 | * 27 | * echo "Failed because: $reason\n"; 28 | * 29 | * }); 30 | * 31 | * Example with coroutines: 32 | * 33 | * coroutine(function() { 34 | * 35 | * try { 36 | * yield $httpClient->request('GET', '/foo'); 37 | * yield $httpClient->request('DELETE', /foo'); 38 | * yield $httpClient->request('PUT', '/foo'); 39 | * } catch(\Throwable $reason) { 40 | * echo "Failed because: $reason\n"; 41 | * } 42 | * 43 | * }); 44 | * 45 | * @return \Sabre\Event\Promise 46 | * 47 | * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 48 | * @author Evert Pot (http://evertpot.com/) 49 | * @license http://sabre.io/license/ Modified BSD License 50 | */ 51 | function coroutine(callable $gen): Promise 52 | { 53 | $generator = $gen(); 54 | if (!$generator instanceof Generator) { 55 | throw new \InvalidArgumentException('You must pass a generator function'); 56 | } 57 | 58 | // This is the value we're returning. 59 | $promise = new Promise(); 60 | 61 | /** 62 | * So tempted to use the mythical y-combinator here, but it's not needed in 63 | * PHP. 64 | */ 65 | $advanceGenerator = function () use (&$advanceGenerator, $generator, $promise) { 66 | while ($generator->valid()) { 67 | $yieldedValue = $generator->current(); 68 | if ($yieldedValue instanceof Promise) { 69 | $yieldedValue->then( 70 | function ($value) use ($generator, &$advanceGenerator) { 71 | $generator->send($value); 72 | $advanceGenerator(); 73 | }, 74 | function (Throwable $reason) use ($generator, $advanceGenerator) { 75 | $generator->throw($reason); 76 | $advanceGenerator(); 77 | } 78 | )->otherwise(function (Throwable $reason) use ($promise) { 79 | // This error handler would be called, if something in the 80 | // generator throws an exception, and it's not caught 81 | // locally. 82 | $promise->reject($reason); 83 | }); 84 | // We need to break out of the loop, because $advanceGenerator 85 | // will be called asynchronously when the promise has a result. 86 | break; 87 | } else { 88 | // If the value was not a promise, we'll just let it pass through. 89 | $generator->send($yieldedValue); 90 | } 91 | } 92 | 93 | // If the generator is at the end, and we didn't run into an exception, 94 | // We're grabbing the "return" value and fulfilling our top-level 95 | // promise with its value. 96 | if (!$generator->valid() && Promise::PENDING === $promise->state) { 97 | $returnValue = $generator->getReturn(); 98 | 99 | // The return value is a promise. 100 | if ($returnValue instanceof Promise) { 101 | $returnValue->then(function ($value) use ($promise) { 102 | $promise->fulfill($value); 103 | }, function (Throwable $reason) use ($promise) { 104 | $promise->reject($reason); 105 | }); 106 | } else { 107 | $promise->fulfill($returnValue); 108 | } 109 | } 110 | }; 111 | 112 | try { 113 | $advanceGenerator(); 114 | } catch (Throwable $e) { 115 | $promise->reject($e); 116 | } 117 | 118 | return $promise; 119 | } 120 | -------------------------------------------------------------------------------- /lib/EmitterTrait.php: -------------------------------------------------------------------------------- 1 | listeners[$eventName])) { 28 | $this->listeners[$eventName] = [ 29 | true, // If there's only one item, it's sorted 30 | [$priority], 31 | [$callBack], 32 | ]; 33 | } else { 34 | $this->listeners[$eventName][0] = false; // marked as unsorted 35 | $this->listeners[$eventName][1][] = $priority; 36 | $this->listeners[$eventName][2][] = $callBack; 37 | } 38 | } 39 | 40 | /** 41 | * Subscribe to an event exactly once. 42 | */ 43 | public function once(string $eventName, callable $callBack, int $priority = 100) 44 | { 45 | $wrapper = null; 46 | $wrapper = function () use ($eventName, $callBack, &$wrapper) { 47 | $this->removeListener($eventName, $wrapper); 48 | 49 | return \call_user_func_array($callBack, \func_get_args()); 50 | }; 51 | 52 | $this->on($eventName, $wrapper, $priority); 53 | } 54 | 55 | /** 56 | * Emits an event. 57 | * 58 | * This method will return true if 0 or more listeners were successfully 59 | * handled. false is returned if one of the events broke the event chain. 60 | * 61 | * If the continueCallBack is specified, this callback will be called every 62 | * time before the next event handler is called. 63 | * 64 | * If the continueCallback returns false, event propagation stops. This 65 | * allows you to use the eventEmitter as a means for listeners to implement 66 | * functionality in your application, and break the event loop as soon as 67 | * some condition is fulfilled. 68 | * 69 | * Note that returning false from an event subscriber breaks propagation 70 | * and returns false, but if the continue-callback stops propagation, this 71 | * is still considered a 'successful' operation and returns true. 72 | * 73 | * Lastly, if there are 5 event handlers for an event. The continueCallback 74 | * will be called at most 4 times. 75 | */ 76 | public function emit(string $eventName, array $arguments = [], callable $continueCallBack = null): bool 77 | { 78 | if (\is_null($continueCallBack)) { 79 | foreach ($this->listeners($eventName) as $listener) { 80 | $result = \call_user_func_array($listener, $arguments); 81 | if (false === $result) { 82 | return false; 83 | } 84 | } 85 | } else { 86 | $listeners = $this->listeners($eventName); 87 | $counter = \count($listeners); 88 | 89 | foreach ($listeners as $listener) { 90 | --$counter; 91 | $result = \call_user_func_array($listener, $arguments); 92 | if (false === $result) { 93 | return false; 94 | } 95 | 96 | if ($counter > 0) { 97 | if (!$continueCallBack()) { 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | 104 | return true; 105 | } 106 | 107 | /** 108 | * Returns the list of listeners for an event. 109 | * 110 | * The list is returned as an array, and the list of events are sorted by 111 | * their priority. 112 | * 113 | * @return callable[] 114 | */ 115 | public function listeners(string $eventName): array 116 | { 117 | if (!isset($this->listeners[$eventName])) { 118 | return []; 119 | } 120 | 121 | // The list is not sorted 122 | if (!$this->listeners[$eventName][0]) { 123 | // Sorting 124 | \array_multisort($this->listeners[$eventName][1], SORT_NUMERIC, $this->listeners[$eventName][2]); 125 | 126 | // Marking the listeners as sorted 127 | $this->listeners[$eventName][0] = true; 128 | } 129 | 130 | return $this->listeners[$eventName][2]; 131 | } 132 | 133 | /** 134 | * Removes a specific listener from an event. 135 | * 136 | * If the listener could not be found, this method will return false. If it 137 | * was removed it will return true. 138 | */ 139 | public function removeListener(string $eventName, callable $listener): bool 140 | { 141 | if (!isset($this->listeners[$eventName])) { 142 | return false; 143 | } 144 | foreach ($this->listeners[$eventName][2] as $index => $check) { 145 | if ($check === $listener) { 146 | unset($this->listeners[$eventName][1][$index]); 147 | unset($this->listeners[$eventName][2][$index]); 148 | 149 | return true; 150 | } 151 | } 152 | 153 | return false; 154 | } 155 | 156 | /** 157 | * Removes all listeners. 158 | * 159 | * If the eventName argument is specified, all listeners for that event are 160 | * removed. If it is not specified, every listener for every event is 161 | * removed. 162 | */ 163 | public function removeAllListeners(string $eventName = null) 164 | { 165 | if (!\is_null($eventName)) { 166 | unset($this->listeners[$eventName]); 167 | } else { 168 | $this->listeners = []; 169 | } 170 | } 171 | 172 | /** 173 | * The list of listeners. 174 | * 175 | * @var array 176 | */ 177 | protected $listeners = []; 178 | } 179 | -------------------------------------------------------------------------------- /lib/WildcardEmitterTrait.php: -------------------------------------------------------------------------------- 1 | wildcardListeners; 33 | } else { 34 | $listeners = &$this->listeners; 35 | } 36 | 37 | // Always fully reset the listener index. This is fairly sane for most 38 | // applications, because there's a clear "event registering" and "event 39 | // emitting" phase, but can be slow if there's a lot adding and removing 40 | // of listeners during emitting of events. 41 | $this->listenerIndex = []; 42 | 43 | if (!isset($listeners[$eventName])) { 44 | $listeners[$eventName] = []; 45 | } 46 | $listeners[$eventName][] = [$priority, $callBack]; 47 | } 48 | 49 | /** 50 | * Subscribe to an event exactly once. 51 | */ 52 | public function once(string $eventName, callable $callBack, int $priority = 100) 53 | { 54 | $wrapper = null; 55 | $wrapper = function () use ($eventName, $callBack, &$wrapper) { 56 | $this->removeListener($eventName, $wrapper); 57 | 58 | return \call_user_func_array($callBack, \func_get_args()); 59 | }; 60 | 61 | $this->on($eventName, $wrapper, $priority); 62 | } 63 | 64 | /** 65 | * Emits an event. 66 | * 67 | * This method will return true if 0 or more listeners were successfully 68 | * handled. false is returned if one of the events broke the event chain. 69 | * 70 | * If the continueCallBack is specified, this callback will be called every 71 | * time before the next event handler is called. 72 | * 73 | * If the continueCallback returns false, event propagation stops. This 74 | * allows you to use the eventEmitter as a means for listeners to implement 75 | * functionality in your application, and break the event loop as soon as 76 | * some condition is fulfilled. 77 | * 78 | * Note that returning false from an event subscriber breaks propagation 79 | * and returns false, but if the continue-callback stops propagation, this 80 | * is still considered a 'successful' operation and returns true. 81 | * 82 | * Lastly, if there are 5 event handlers for an event. The continueCallback 83 | * will be called at most 4 times. 84 | */ 85 | public function emit(string $eventName, array $arguments = [], callable $continueCallBack = null): bool 86 | { 87 | if (\is_null($continueCallBack)) { 88 | foreach ($this->listeners($eventName) as $listener) { 89 | $result = \call_user_func_array($listener, $arguments); 90 | if (false === $result) { 91 | return false; 92 | } 93 | } 94 | } else { 95 | $listeners = $this->listeners($eventName); 96 | $counter = \count($listeners); 97 | 98 | foreach ($listeners as $listener) { 99 | --$counter; 100 | $result = \call_user_func_array($listener, $arguments); 101 | if (false === $result) { 102 | return false; 103 | } 104 | 105 | if ($counter > 0) { 106 | if (!$continueCallBack()) { 107 | break; 108 | } 109 | } 110 | } 111 | } 112 | 113 | return true; 114 | } 115 | 116 | /** 117 | * Returns the list of listeners for an event. 118 | * 119 | * The list is returned as an array, and the list of events are sorted by 120 | * their priority. 121 | * 122 | * @return callable[] 123 | */ 124 | public function listeners(string $eventName): array 125 | { 126 | if (!\array_key_exists($eventName, $this->listenerIndex)) { 127 | // Create a new index. 128 | $listeners = []; 129 | $listenersPriority = []; 130 | if (isset($this->listeners[$eventName])) { 131 | foreach ($this->listeners[$eventName] as $listener) { 132 | $listenersPriority[] = $listener[0]; 133 | $listeners[] = $listener[1]; 134 | } 135 | } 136 | 137 | foreach ($this->wildcardListeners as $wcEvent => $wcListeners) { 138 | // Wildcard match 139 | if (\substr($eventName, 0, \strlen($wcEvent)) === $wcEvent) { 140 | foreach ($wcListeners as $listener) { 141 | $listenersPriority[] = $listener[0]; 142 | $listeners[] = $listener[1]; 143 | } 144 | } 145 | } 146 | 147 | // Sorting by priority 148 | \array_multisort($listenersPriority, SORT_NUMERIC, $listeners); 149 | 150 | // Creating index 151 | $this->listenerIndex[$eventName] = $listeners; 152 | } 153 | 154 | return $this->listenerIndex[$eventName]; 155 | } 156 | 157 | /** 158 | * Removes a specific listener from an event. 159 | * 160 | * If the listener could not be found, this method will return false. If it 161 | * was removed it will return true. 162 | */ 163 | public function removeListener(string $eventName, callable $listener): bool 164 | { 165 | // If it ends with a wildcard, we use the wildcardListeners array 166 | if ('*' === $eventName[\strlen($eventName) - 1]) { 167 | $eventName = \substr($eventName, 0, -1); 168 | $listeners = &$this->wildcardListeners; 169 | } else { 170 | $listeners = &$this->listeners; 171 | } 172 | 173 | if (!isset($listeners[$eventName])) { 174 | return false; 175 | } 176 | 177 | foreach ($listeners[$eventName] as $index => $check) { 178 | if ($check[1] === $listener) { 179 | // Remove listener 180 | unset($listeners[$eventName][$index]); 181 | // Reset index 182 | $this->listenerIndex = []; 183 | 184 | return true; 185 | } 186 | } 187 | 188 | return false; 189 | } 190 | 191 | /** 192 | * Removes all listeners. 193 | * 194 | * If the eventName argument is specified, all listeners for that event are 195 | * removed. If it is not specified, every listener for every event is 196 | * removed. 197 | */ 198 | public function removeAllListeners(string $eventName = null) 199 | { 200 | if (\is_null($eventName)) { 201 | $this->listeners = []; 202 | $this->wildcardListeners = []; 203 | } else { 204 | if ('*' === $eventName[\strlen($eventName) - 1]) { 205 | // Wildcard event 206 | unset($this->wildcardListeners[\substr($eventName, 0, -1)]); 207 | } else { 208 | unset($this->listeners[$eventName]); 209 | } 210 | } 211 | 212 | // Reset index 213 | $this->listenerIndex = []; 214 | } 215 | 216 | /** 217 | * The list of listeners. 218 | */ 219 | protected $listeners = []; 220 | 221 | /** 222 | * The list of "wildcard listeners". 223 | */ 224 | protected $wildcardListeners = []; 225 | 226 | /** 227 | * An index of listeners for a specific event name. This helps speeding 228 | * up emitting events after all listeners have been set. 229 | * 230 | * If the list of listeners changes though, the index clears. 231 | */ 232 | protected $listenerIndex = []; 233 | } 234 | -------------------------------------------------------------------------------- /lib/Loop/Loop.php: -------------------------------------------------------------------------------- 1 | timers) { 30 | // Special case when the timers array was empty. 31 | $this->timers[] = [$triggerTime, $cb]; 32 | 33 | return; 34 | } 35 | 36 | // We need to insert these values in the timers array, but the timers 37 | // array must be in reverse-order of trigger times. 38 | // 39 | // So here we search the array for the insertion point. 40 | $index = count($this->timers) - 1; 41 | while (true) { 42 | if ($triggerTime < $this->timers[$index][0]) { 43 | array_splice( 44 | $this->timers, 45 | $index + 1, 46 | 0, 47 | [[$triggerTime, $cb]] 48 | ); 49 | break; 50 | } elseif (0 === $index) { 51 | array_unshift($this->timers, [$triggerTime, $cb]); 52 | break; 53 | } 54 | --$index; 55 | } 56 | } 57 | 58 | /** 59 | * Executes a function every x seconds. 60 | * 61 | * The value this function returns can be used to stop the interval with 62 | * clearInterval. 63 | */ 64 | public function setInterval(callable $cb, float $timeout): array 65 | { 66 | $keepGoing = true; 67 | $f = null; 68 | 69 | $f = function () use ($cb, &$f, $timeout, &$keepGoing) { 70 | if ($keepGoing) { 71 | $cb(); 72 | $this->setTimeout($f, $timeout); 73 | } 74 | }; 75 | $this->setTimeout($f, $timeout); 76 | 77 | // Really the only thing that matters is returning the $keepGoing 78 | // boolean value. 79 | // 80 | // We need to pack it in an array to allow returning by reference. 81 | // Because I'm worried people will be confused by using a boolean as a 82 | // sort of identifier, I added an extra string. 83 | return ['I\'m an implementation detail', &$keepGoing]; 84 | } 85 | 86 | /** 87 | * Stops a running interval. 88 | */ 89 | public function clearInterval(array $intervalId) 90 | { 91 | $intervalId[1] = false; 92 | } 93 | 94 | /** 95 | * Runs a function immediately at the next iteration of the loop. 96 | */ 97 | public function nextTick(callable $cb) 98 | { 99 | $this->nextTick[] = $cb; 100 | } 101 | 102 | /** 103 | * Adds a read stream. 104 | * 105 | * The callback will be called as soon as there is something to read from 106 | * the stream. 107 | * 108 | * You MUST call removeReadStream after you are done with the stream, to 109 | * prevent the eventloop from never stopping. 110 | * 111 | * @param resource $stream 112 | */ 113 | public function addReadStream($stream, callable $cb) 114 | { 115 | $this->readStreams[(int) $stream] = $stream; 116 | $this->readCallbacks[(int) $stream] = $cb; 117 | } 118 | 119 | /** 120 | * Adds a write stream. 121 | * 122 | * The callback will be called as soon as the system reports it's ready to 123 | * receive writes on the stream. 124 | * 125 | * You MUST call removeWriteStream after you are done with the stream, to 126 | * prevent the eventloop from never stopping. 127 | * 128 | * @param resource $stream 129 | */ 130 | public function addWriteStream($stream, callable $cb) 131 | { 132 | $this->writeStreams[(int) $stream] = $stream; 133 | $this->writeCallbacks[(int) $stream] = $cb; 134 | } 135 | 136 | /** 137 | * Stop watching a stream for reads. 138 | * 139 | * @param resource $stream 140 | */ 141 | public function removeReadStream($stream) 142 | { 143 | unset( 144 | $this->readStreams[(int) $stream], 145 | $this->readCallbacks[(int) $stream] 146 | ); 147 | } 148 | 149 | /** 150 | * Stop watching a stream for writes. 151 | * 152 | * @param resource $stream 153 | */ 154 | public function removeWriteStream($stream) 155 | { 156 | unset( 157 | $this->writeStreams[(int) $stream], 158 | $this->writeCallbacks[(int) $stream] 159 | ); 160 | } 161 | 162 | /** 163 | * Runs the loop. 164 | * 165 | * This function will run continuously, until there's no more events to 166 | * handle. 167 | */ 168 | public function run() 169 | { 170 | $this->running = true; 171 | 172 | do { 173 | $hasEvents = $this->tick(true); 174 | } while ($this->running && $hasEvents); 175 | $this->running = false; 176 | } 177 | 178 | /** 179 | * Executes all pending events. 180 | * 181 | * If $block is turned true, this function will block until any event is 182 | * triggered. 183 | * 184 | * If there are now timeouts, nextTick callbacks or events in the loop at 185 | * all, this function will exit immediately. 186 | * 187 | * This function will return true if there are _any_ events left in the 188 | * loop after the tick. 189 | */ 190 | public function tick(bool $block = false): bool 191 | { 192 | $this->runNextTicks(); 193 | $nextTimeout = $this->runTimers(); 194 | 195 | // Calculating how long runStreams should at most wait. 196 | if (!$block) { 197 | // Don't wait 198 | $streamWait = 0; 199 | } elseif ($this->nextTick) { 200 | // There's a pending 'nextTick'. Don't wait. 201 | $streamWait = 0; 202 | } elseif (is_numeric($nextTimeout)) { 203 | // Wait until the next Timeout should trigger. 204 | $streamWait = $nextTimeout; 205 | } else { 206 | // Wait indefinitely 207 | $streamWait = null; 208 | } 209 | 210 | $this->runStreams($streamWait); 211 | 212 | return $this->readStreams || $this->writeStreams || $this->nextTick || $this->timers; 213 | } 214 | 215 | /** 216 | * Stops a running eventloop. 217 | */ 218 | public function stop() 219 | { 220 | $this->running = false; 221 | } 222 | 223 | /** 224 | * Executes all 'nextTick' callbacks. 225 | * 226 | * return void 227 | */ 228 | protected function runNextTicks() 229 | { 230 | $nextTick = $this->nextTick; 231 | $this->nextTick = []; 232 | 233 | foreach ($nextTick as $cb) { 234 | $cb(); 235 | } 236 | } 237 | 238 | /** 239 | * Runs all pending timers. 240 | * 241 | * After running the timer callbacks, this function returns the number of 242 | * seconds until the next timer should be executed. 243 | * 244 | * If there's no more pending timers, this function returns null. 245 | * 246 | * @return float|null 247 | */ 248 | protected function runTimers() 249 | { 250 | $now = microtime(true); 251 | while (($timer = array_pop($this->timers)) && $timer[0] < $now) { 252 | $timer[1](); 253 | } 254 | // Add the last timer back to the array. 255 | if ($timer) { 256 | $this->timers[] = $timer; 257 | 258 | return max(0, $timer[0] - microtime(true)); 259 | } 260 | } 261 | 262 | /** 263 | * Runs all pending stream events. 264 | * 265 | * If $timeout is 0, it will return immediately. If $timeout is null, it 266 | * will wait indefinitely. 267 | * 268 | * @param float|null timeout 269 | */ 270 | protected function runStreams($timeout) 271 | { 272 | if ($this->readStreams || $this->writeStreams) { 273 | $read = $this->readStreams; 274 | $write = $this->writeStreams; 275 | $except = null; 276 | if (stream_select($read, $write, $except, (null === $timeout) ? null : 0, $timeout ? (int) ($timeout * 1000000) : 0)) { 277 | // See PHP Bug https://bugs.php.net/bug.php?id=62452 278 | // Fixed in PHP7 279 | foreach ($read as $readStream) { 280 | $readCb = $this->readCallbacks[(int) $readStream]; 281 | $readCb(); 282 | } 283 | foreach ($write as $writeStream) { 284 | $writeCb = $this->writeCallbacks[(int) $writeStream]; 285 | $writeCb(); 286 | } 287 | } 288 | } elseif ($this->running && ($this->nextTick || $this->timers)) { 289 | usleep(null !== $timeout ? intval($timeout * 1000000) : 200000); 290 | } 291 | } 292 | 293 | /** 294 | * Is the main loop active. 295 | * 296 | * @var bool 297 | */ 298 | protected $running = false; 299 | 300 | /** 301 | * A list of timers, added by setTimeout. 302 | * 303 | * @var array 304 | */ 305 | protected $timers = []; 306 | 307 | /** 308 | * A list of 'nextTick' callbacks. 309 | * 310 | * @var callable[] 311 | */ 312 | protected $nextTick = []; 313 | 314 | /** 315 | * List of readable streams for stream_select, indexed by stream id. 316 | * 317 | * @var resource[] 318 | */ 319 | protected $readStreams = []; 320 | 321 | /** 322 | * List of writable streams for stream_select, indexed by stream id. 323 | * 324 | * @var resource[] 325 | */ 326 | protected $writeStreams = []; 327 | 328 | /** 329 | * List of read callbacks, indexed by stream id. 330 | * 331 | * @var callable[] 332 | */ 333 | protected $readCallbacks = []; 334 | 335 | /** 336 | * List of write callbacks, indexed by stream id. 337 | * 338 | * @var callable[] 339 | */ 340 | protected $writeCallbacks = []; 341 | } 342 | -------------------------------------------------------------------------------- /lib/Promise.php: -------------------------------------------------------------------------------- 1 | fulfill and $this->reject. 63 | * Using the executor is optional. 64 | */ 65 | public function __construct( ...$executor) 66 | { 67 | $callExecutor = isset($executor[0]) ? $executor[0] : null; 68 | $childLoop = $this->isEventLoopAvailable($callExecutor) ? $callExecutor : null; 69 | $callExecutor = $this->isEventLoopAvailable($callExecutor) ? null : $callExecutor; 70 | 71 | $callCanceller = isset($executor[1]) ? $executor[1] : null; 72 | $childLoop = $this->isEventLoopAvailable($callCanceller) ? $callCanceller : $childLoop; 73 | $callCanceller = $this->isEventLoopAvailable($callCanceller) ? null : $callCanceller; 74 | 75 | $loop = isset($executor[2]) ? $executor[2] : null; 76 | $childLoop = $this->isEventLoopAvailable($loop) ? $loop : $childLoop; 77 | $this->loop = $this->isEventLoopAvailable($childLoop) ? $childLoop : Loop\instance(); 78 | 79 | /** 80 | * The difference in Guzzle promise implementations mainly lay in the construction. 81 | * According to https://github.com/promises-aplus/constructor-spec/issues/18 it's not valid. 82 | * 83 | * The wait method in Guzzle is necessary under certain callable functions situations. 84 | * Mainly when passing an promise object not fully created, itself. 85 | * 86 | * The constructor will fail it's execution when trying to access member method that's null: 87 | * 88 | * $callableAndNullMethodAccess = new Promise(function () use (&$callableAndNullMethodAccess){ 89 | * $callableAndNullMethodAccess->resolve('Runtime Error'); 90 | * }); 91 | * 92 | * 93 | * The following routine adds the callback resolver to the event loop. 94 | * 95 | * And since an promise is attached to an running event loop, 96 | * no need to start the promises fate. The wait function/method Guzzle have both starts 97 | * and stops promise execution, however the promise implementation should still 98 | * be able run without, should be an optional execution point controlled by the developer. 99 | */ 100 | $this->waitFn = is_callable($callExecutor) ? $callExecutor : null; 101 | $this->cancelFn = is_callable($callCanceller) ? $callCanceller : null; 102 | 103 | $promiseFunction = function () use($callExecutor) { 104 | if (is_callable($callExecutor)) { 105 | $callExecutor([$this, 'fulfill'], [$this, 'reject']); 106 | } 107 | }; 108 | 109 | try { 110 | $promiseFunction(); 111 | } catch (\Throwable $e) { 112 | $this->isWaitRequired = true; 113 | $this->implement($promiseFunction); 114 | } catch (\Exception $exception) { 115 | $this->isWaitRequired = true; 116 | $this->implement($promiseFunction); 117 | } 118 | } 119 | 120 | private function isEventLoopAvailable($instance = null): bool 121 | { 122 | $isInstanceiable = false; 123 | if ($instance instanceof TaskQueueInterface) 124 | $isInstanceiable = true; 125 | elseif ($instance instanceof LoopInterface) 126 | $isInstanceiable = true; 127 | elseif ($instance instanceof Loop) 128 | $isInstanceiable = true; 129 | 130 | return $isInstanceiable; 131 | } 132 | 133 | public function getState() 134 | { 135 | return $this->state; 136 | } 137 | 138 | /** 139 | * This method allows you to specify the callback that will be called after 140 | * the promise has been fulfilled or rejected. 141 | * 142 | * Both arguments are optional. 143 | * 144 | * This method returns a new promise, which can be used for chaining. 145 | * If either the onFulfilled or onRejected callback is called, you may 146 | * return a result from this callback. 147 | * 148 | * If the result of this callback is yet another promise, the result of 149 | * _that_ promise will be used to set the result of the returned promise. 150 | * 151 | * If either of the callbacks return any other value, the returned promise 152 | * is automatically fulfilled with that value. 153 | * 154 | * If either of the callbacks throw an exception, the returned promise will 155 | * be rejected and the exception will be passed back. 156 | */ 157 | public function then(callable $onFulfilled = null, callable $onRejected = null): Promise 158 | { 159 | // This new subPromise will be returned from this function, and will 160 | // be fulfilled with the result of the onFulfilled or onRejected event 161 | // handlers. 162 | $subPromise = new Promise(null, [$this, 'cancel']); 163 | 164 | switch ($this->state) { 165 | case self::PENDING: 166 | // The operation is pending, so we keep a reference to the 167 | // event handlers so we can call them later. 168 | $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; 169 | break; 170 | case self::FULFILLED: 171 | // The async operation is already fulfilled, so we trigger the 172 | // onFulfilled callback asap. 173 | $this->invokeCallback($subPromise, $onFulfilled); 174 | break; 175 | case self::REJECTED: 176 | // The async operation failed, so we call the onRejected 177 | // callback asap. 178 | $this->invokeCallback($subPromise, $onRejected); 179 | break; 180 | } 181 | 182 | return $subPromise; 183 | } 184 | 185 | /** 186 | * Add a callback for when this promise is rejected. 187 | * 188 | * Its usage is identical to then(). However, the otherwise() function is 189 | * preferred. 190 | */ 191 | public function otherwise(callable $onRejected): Promise 192 | { 193 | return $this->then(null, $onRejected); 194 | } 195 | 196 | public function resolve($value = null) 197 | { 198 | if ($value instanceof Promise) { 199 | return $value->then(); 200 | } 201 | 202 | return $this->fulfill($value); 203 | } 204 | 205 | /** 206 | * Marks this promise as fulfilled and sets its return value. 207 | * 208 | * @param mixed $value 209 | */ 210 | public function fulfill($value = null) 211 | { 212 | if (self::PENDING !== $this->state) { 213 | throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); 214 | } 215 | $this->state = self::FULFILLED; 216 | $this->value = $value; 217 | foreach ($this->subscribers as $subscriber) { 218 | $this->invokeCallback($subscriber[0], $subscriber[1]); 219 | } 220 | } 221 | 222 | /** 223 | * Marks this promise as rejected, and set it's rejection reason. 224 | */ 225 | public function reject($reason) 226 | { 227 | if (self::PENDING !== $this->state) { 228 | throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); 229 | } 230 | $this->state = self::REJECTED; 231 | $this->value = $reason; 232 | foreach ($this->subscribers as $subscriber) { 233 | $this->invokeCallback($subscriber[0], $subscriber[2]); 234 | } 235 | } 236 | 237 | public function cancel() 238 | { 239 | if (self::PENDING !== $this->state) { 240 | return; 241 | } 242 | 243 | $this->waitFn = null; 244 | $this->subscribers = []; 245 | 246 | if ($this->cancelFn) { 247 | $fn = $this->cancelFn; 248 | $this->cancelFn = null; 249 | try { 250 | $fn(); 251 | } catch (Throwable $e) { 252 | $this->reject($e); 253 | } catch (Exception $exception) { 254 | $this->reject($exception); 255 | } 256 | } 257 | 258 | // Reject the promise only if it wasn't rejected in a then callback. 259 | if (self::PENDING === $this->state) { 260 | $this->reject(new CancellationException('Promise has been cancelled')); 261 | } 262 | } 263 | /** 264 | * Stops execution until this promise is resolved. 265 | * 266 | * This method stops execution completely. If the promise is successful with 267 | * a value, this method will return this value. If the promise was 268 | * rejected, this method will throw an exception. 269 | * 270 | * This effectively turns the asynchronous operation into a synchronous 271 | * one. In PHP it might be useful to call this on the last promise in a 272 | * chain. 273 | * 274 | * @return mixed 275 | */ 276 | public function wait($unwrap = true) 277 | { 278 | try { 279 | $loop = $this->loop; 280 | $fn = $this->waitFn; 281 | $this->waitFn = null; 282 | if (is_callable($fn) 283 | && method_exists($loop, 'add') 284 | && method_exists($loop, 'run') 285 | && $this->isWaitRequired 286 | ) { 287 | $this->isWaitRequired = false; 288 | $fn([$this, 'fulfill'], [$this, 'reject']); 289 | $loop->run(); 290 | } elseif (method_exists($loop, 'tick')) { 291 | $hasEvents = true; 292 | while (self::PENDING === $this->state) { 293 | if (!$hasEvents) { 294 | throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); 295 | } 296 | 297 | // As long as the promise is not fulfilled, we tell the event loop 298 | // to handle events, and to block. 299 | $hasEvents = $loop->tick(true); 300 | } 301 | } 302 | } catch (\Exception $reason) { 303 | if ($this->state === self::PENDING) { 304 | // The promise has not been resolved yet, so reject the promise 305 | // with the exception. 306 | $this->reject($reason); 307 | } else { 308 | // The promise was already resolved, so there's a problem in 309 | // the application. 310 | throw $reason; 311 | } 312 | } 313 | 314 | if ($this->value instanceof Promise) { 315 | return $this->value->wait($unwrap); 316 | } 317 | 318 | $result = $this->value; 319 | 320 | if ($this->state === self::PENDING) { 321 | $this->reject('Invoking the wait callback did not resolve the promise'); 322 | } elseif (self::FULFILLED === $this->state) { 323 | // If the state of this promise is fulfilled, we can return the value. 324 | return $result; 325 | } elseif ($unwrap) { 326 | // If we got here, it means that the asynchronous operation 327 | // errored. Therefore we need to throw an exception. 328 | if ($result instanceof Exception) { 329 | throw $result; 330 | } elseif (is_scalar($result)) { 331 | throw new \Exception($result); 332 | } else { 333 | $type = is_object($result) ? get_class($result) : gettype($result); 334 | throw new \Exception('Promise was rejected with reason of type: ' . $type); 335 | } 336 | } 337 | } 338 | 339 | /** 340 | * A list of subscribers. Subscribers are the callbacks that want us to let 341 | * them know if the callback was fulfilled or rejected. 342 | * 343 | * @var array 344 | */ 345 | protected $subscribers = []; 346 | 347 | /** 348 | * The result of the promise. 349 | * 350 | * If the promise was fulfilled, this will be the result value. If the 351 | * promise was rejected, this property hold the rejection reason. 352 | * 353 | * @var mixed 354 | */ 355 | protected $value = null; 356 | protected $cancelFn = null; 357 | protected $waitFn = null; 358 | 359 | /** 360 | * This method is used to call either an onFulfilled or onRejected callback. 361 | * 362 | * This method makes sure that the result of these callbacks are handled 363 | * correctly, and any chained promises are also correctly fulfilled or 364 | * rejected. 365 | * 366 | * @param Promise $subPromise 367 | * @param callable $callBack 368 | */ 369 | private function invokeCallback(Promise $subPromise, callable $callBack = null) 370 | { 371 | // We use 'nextTick' to ensure that the event handlers are always 372 | // triggered outside of the calling stack in which they were originally 373 | // passed to 'then'. 374 | // 375 | // This makes the order of execution more predictable. 376 | $promiseFunction = function() use ($callBack, $subPromise) { 377 | if (is_callable($callBack)) { 378 | try { 379 | $result = $callBack($this->value); 380 | if ($result instanceof self) { 381 | // If the callback (onRejected or onFulfilled) 382 | // returned a promise, we only fulfill or reject the 383 | // chained promise once that promise has also been 384 | // resolved. 385 | $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); 386 | } else { 387 | // If the callback returned any other value, we 388 | // immediately fulfill the chained promise. 389 | $subPromise->fulfill($result); 390 | } 391 | } catch (Throwable $e) { 392 | // If the event handler threw an exception, we need to make sure that 393 | // the chained promise is rejected as well. 394 | $subPromise->reject($e); 395 | } catch (Exception $exception) { 396 | $subPromise->reject($exception); 397 | } 398 | } else { 399 | if (self::FULFILLED === $this->state) { 400 | $subPromise->fulfill($this->value); 401 | } else { 402 | $subPromise->reject($this->value); 403 | } 404 | } 405 | }; 406 | 407 | $this->implement($promiseFunction, $subPromise); 408 | } 409 | 410 | public function implement(callable $function, Promise $promise = null) 411 | { 412 | if ($this->loop) { 413 | $loop = $this->loop; 414 | 415 | $othersLoop = null; 416 | if (method_exists($loop, 'futureTick')) 417 | $othersLoop = [$loop, 'futureTick']; 418 | elseif (method_exists($loop, 'addTick')) 419 | $othersLoop = [$loop, 'addTick']; 420 | elseif (method_exists($loop, 'onTick')) 421 | $othersLoop = [$loop, 'onTick']; 422 | elseif (method_exists($loop, 'enqueue')) 423 | $othersLoop = [$loop, 'enqueue']; 424 | elseif (method_exists($loop, 'add')) 425 | $othersLoop = [$loop, 'add']; 426 | 427 | if ($othersLoop) 428 | call_user_func($othersLoop, $function); 429 | else 430 | $loop->nextTick($function); 431 | } else { 432 | return $function(); 433 | } 434 | 435 | return $promise; 436 | } 437 | } 438 | --------------------------------------------------------------------------------