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