1<?php 2 3namespace GuzzleHttp\Promise; 4 5use Exception; 6use Generator; 7use Throwable; 8 9/** 10 * Creates a promise that is resolved using a generator that yields values or 11 * promises (somewhat similar to C#'s async keyword). 12 * 13 * When called, the Coroutine::of method will start an instance of the generator 14 * and returns a promise that is fulfilled with its final yielded value. 15 * 16 * Control is returned back to the generator when the yielded promise settles. 17 * This can lead to less verbose code when doing lots of sequential async calls 18 * with minimal processing in between. 19 * 20 * use GuzzleHttp\Promise; 21 * 22 * function createPromise($value) { 23 * return new Promise\FulfilledPromise($value); 24 * } 25 * 26 * $promise = Promise\Coroutine::of(function () { 27 * $value = (yield createPromise('a')); 28 * try { 29 * $value = (yield createPromise($value . 'b')); 30 * } catch (\Exception $e) { 31 * // The promise was rejected. 32 * } 33 * yield $value . 'c'; 34 * }); 35 * 36 * // Outputs "abc" 37 * $promise->then(function ($v) { echo $v; }); 38 * 39 * @param callable $generatorFn Generator function to wrap into a promise. 40 * 41 * @return Promise 42 * 43 * @link https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration 44 */ 45final class Coroutine implements PromiseInterface 46{ 47 /** 48 * @var PromiseInterface|null 49 */ 50 private $currentPromise; 51 52 /** 53 * @var Generator 54 */ 55 private $generator; 56 57 /** 58 * @var Promise 59 */ 60 private $result; 61 62 public function __construct(callable $generatorFn) 63 { 64 $this->generator = $generatorFn(); 65 $this->result = new Promise(function () { 66 while (isset($this->currentPromise)) { 67 $this->currentPromise->wait(); 68 } 69 }); 70 try { 71 $this->nextCoroutine($this->generator->current()); 72 } catch (\Exception $exception) { 73 $this->result->reject($exception); 74 } catch (Throwable $throwable) { 75 $this->result->reject($throwable); 76 } 77 } 78 79 /** 80 * Create a new coroutine. 81 * 82 * @return self 83 */ 84 public static function of(callable $generatorFn) 85 { 86 return new self($generatorFn); 87 } 88 89 public function then( 90 callable $onFulfilled = null, 91 callable $onRejected = null 92 ) { 93 return $this->result->then($onFulfilled, $onRejected); 94 } 95 96 public function otherwise(callable $onRejected) 97 { 98 return $this->result->otherwise($onRejected); 99 } 100 101 public function wait($unwrap = true) 102 { 103 return $this->result->wait($unwrap); 104 } 105 106 public function getState() 107 { 108 return $this->result->getState(); 109 } 110 111 public function resolve($value) 112 { 113 $this->result->resolve($value); 114 } 115 116 public function reject($reason) 117 { 118 $this->result->reject($reason); 119 } 120 121 public function cancel() 122 { 123 $this->currentPromise->cancel(); 124 $this->result->cancel(); 125 } 126 127 private function nextCoroutine($yielded) 128 { 129 $this->currentPromise = Create::promiseFor($yielded) 130 ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); 131 } 132 133 /** 134 * @internal 135 */ 136 public function _handleSuccess($value) 137 { 138 unset($this->currentPromise); 139 try { 140 $next = $this->generator->send($value); 141 if ($this->generator->valid()) { 142 $this->nextCoroutine($next); 143 } else { 144 $this->result->resolve($value); 145 } 146 } catch (Exception $exception) { 147 $this->result->reject($exception); 148 } catch (Throwable $throwable) { 149 $this->result->reject($throwable); 150 } 151 } 152 153 /** 154 * @internal 155 */ 156 public function _handleFailure($reason) 157 { 158 unset($this->currentPromise); 159 try { 160 $nextYield = $this->generator->throw(Create::exceptionFor($reason)); 161 // The throw was caught, so keep iterating on the coroutine 162 $this->nextCoroutine($nextYield); 163 } catch (Exception $exception) { 164 $this->result->reject($exception); 165 } catch (Throwable $throwable) { 166 $this->result->reject($throwable); 167 } 168 } 169} 170