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