Promise::calling()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 5
c 1
b 0
f 1
nc 3
nop 3
dl 0
loc 7
rs 10
1
<?php
2
/**
3
 * Promise EXPR
4
 * User: moyo
5
 * Date: 03/08/2017
6
 * Time: 11:54 AM
7
 */
8
9
namespace Carno\Promise;
10
11
use Carno\Promise\Exception\InvalidState;
12
use Carno\Promise\Features\All;
13
use Carno\Promise\Features\Deferred;
14
use Carno\Promise\Features\Fusion;
15
use Carno\Promise\Features\Race;
16
use Carno\Promise\Features\Settled;
17
use Carno\Promise\Features\Sync;
18
use Closure;
19
use SplStack;
20
use Throwable;
21
use TypeError;
22
23
final class Promise implements Promised
24
{
25
    use All;
26
    use Race;
27
    use Settled;
28
    use Deferred;
29
    use Sync;
30
    use Fusion;
31
32
    /**
33
     * PENDING is initial state
34
     */
35
    private const PENDING = 0xF0;
36
    private const FULFILLED = 0xF1;
37
    private const REJECTED = 0xF2;
38
39
    /**
40
     * @var int
41
     */
42
    private $state = self::PENDING;
43
44
    /**
45
     * @var bool
46
     */
47
    private $ack = false;
48
49
    /**
50
     * @var SplStack
51
     */
52
    private $stack = null;
53
54
    /**
55
     * @var bool
56
     */
57
    private $chained = false;
58
59
    /**
60
     * @var mixed
61
     */
62
    private $result = null;
63
64
    /**
65
     * Promise constructor.
66
     * @param callable $executor
67
     */
68
    public function __construct(?callable $executor)
69
    {
70
        Stats::proposed($this);
71
        $this->stack = new SplStack();
72
        $executor && $this->calling($executor, $this, [$this]);
73
    }
74
75
    /**
76
     */
77
    public function __destruct()
78
    {
79
        Stats::confirmed($this);
80
    }
81
82
    /**
83
     * @return bool
84
     */
85
    public function pended() : bool
86
    {
87
        return ! $this->ack;
88
    }
89
90
    /**
91
     * @return bool
92
     */
93
    public function chained() : bool
94
    {
95
        return $this->chained;
96
    }
97
98
    /**
99
     * @param callable $onFulfilled
100
     * @param callable $onRejected
101
     * @return Promised
102
     */
103
    public function then(callable $onFulfilled = null, callable $onRejected = null) : Promised
104
    {
105
        $this->chained = true;
106
107
        $next = new self(null);
108
109
        $this->stack->push([$onFulfilled, $onRejected, $next]);
110
111
        $this->pended() || $this->settle();
112
113
        return $next;
114
    }
115
116
    /**
117
     * @param callable $onThrowing
118
     * @return Promised
119
     */
120
    public function catch(callable $onThrowing) : Promised
121
    {
122
        return $this->then(null, $onThrowing);
123
    }
124
125
    /**
126
     * @param mixed ...$args
127
     */
128
    public function resolve(...$args) : void
129
    {
130
        $this->settle(self::FULFILLED, $args);
131
    }
132
133
    /**
134
     * @param mixed ...$args
135
     * @throws Throwable
136
     */
137
    public function reject(...$args) : void
138
    {
139
        $this->settle(self::REJECTED, $args);
140
    }
141
142
    /**
143
     * @param Throwable $error
144
     * @throws Throwable
145
     */
146
    public function throw(Throwable $error) : void
147
    {
148
        $this->reject($error);
149
    }
150
151
    /**
152
     * @param callable $executor
153
     * @param Promised $current
154
     * @param array $arguments
155
     * @return array [result:mixed, error:throwable]
156
     */
157
    private function calling(callable $executor, Promised $current, array $arguments) : array
158
    {
159
        try {
160
            return [call_user_func($executor, ...$arguments), null];
161
        } catch (Throwable $error) {
162
            $current->pended() && $current->throw($error);
163
            return [null, $error];
164
        }
165
    }
166
167
    /**
168
     * @param Promised $promised
169
     * @param int $type
170
     * @return Closure
171
     */
172
    private function resolver(Promised $promised, int $type) : Closure
173
    {
174
        return static function (...$args) use ($promised, $type) {
175
            $type === self::FULFILLED
176
                ? $promised->resolve(...$args)
177
                : $promised->reject(...$args)
178
            ;
179
        };
180
    }
181
182
    /**
183
     * @param int $state
184
     * @param array $args
185
     * @return Promised
186
     * @throws Throwable
187
     * @throws TypeError
188
     */
189
    private function settle(int $state = null, array $args = []) : Promised
190
    {
191
        if (is_null($state)) {
192
            $result = $this->result;
193
        } else {
194
            if ($this->ack) {
195
                throw new InvalidState('Promise is already confirmed');
196
            }
197
            $this->ack = true;
198
            $this->state = $state;
199
            $this->result = $result = $args;
200
        }
201
202
        $resolved = $this->state === self::FULFILLED;
203
204
        /**
205
         * @var Promised $next
206
         */
207
208
        while (!$this->stack->isEmpty()) {
209
            list($onFulfilled, $onRejected, $next) = $this->stack->shift();
210
211
            if (null === $executor = $resolved ? $onFulfilled : $onRejected) {
212
                $resolved ? $next->resolve(...$result) : $next->reject(...$result);
213
                continue;
214
            }
215
216
            list($stash, $failure) = $this->calling($executor, $next, $result);
217
218
            if ($failure) {
219
                if ($this->fusion || !$next->chained()) {
220
                    throw $failure;
221
                }
222
            } elseif ($stash instanceof Promised) {
223
                if ($stash === $this) {
224
                    throw new TypeError('Response promise is same with current');
225
                } else {
226
                    $stash->then($this->resolver($next, self::FULFILLED), $this->resolver($next, self::REJECTED));
227
                }
228
            } elseif ($stash instanceof Closure) {
229
                $this->calling($stash, $next, [$next]);
230
            } else {
231
                $resolved ? $next->resolve($stash) : $next->reject($stash);
232
            }
233
        }
234
235
        return $this;
236
    }
237
}
238