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