Completed
Push — master ( 8d12ff...458df2 )
by Guillaume
02:56
created

Promise   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 272
rs 7.0642
c 0
b 0
f 0
wmc 54
lcom 1
cbo 2

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
B then() 0 24 5
A otherwise() 0 4 1
B wait() 0 19 5
A getState() 0 4 1
B cancel() 0 25 6
A resolve() 0 4 1
A reject() 0 4 1
C settle() 0 59 14
B callHandler() 0 27 6
B waitIfPending() 0 22 5
A invokeWaitFn() 0 18 3
B invokeWaitList() 0 20 5

How to fix   Complexity   

Complex Class

Complex classes like Promise often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Promise, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace GuzzleHttp\Promise;
3
4
/**
5
 * Promises/A+ implementation that avoids recursion when possible.
6
 *
7
 * @link https://promisesaplus.com/
8
 */
9
class Promise implements PromiseInterface
10
{
11
    private $state = self::PENDING;
12
    private $result;
13
    private $cancelFn;
14
    private $waitFn;
15
    private $waitList;
16
    private $handlers = [];
17
18
    /**
19
     * @param callable $waitFn   Fn that when invoked resolves the promise.
20
     * @param callable $cancelFn Fn that when invoked cancels the promise.
21
     */
22
    public function __construct(
23
        callable $waitFn = null,
24
        callable $cancelFn = null
25
    ) {
26
        $this->waitFn = $waitFn;
27
        $this->cancelFn = $cancelFn;
28
    }
29
30
    public function then(
31
        callable $onFulfilled = null,
32
        callable $onRejected = null
33
    ) {
34
        if ($this->state === self::PENDING) {
35
            $p = new Promise(null, [$this, 'cancel']);
36
            $this->handlers[] = [$p, $onFulfilled, $onRejected];
37
            $p->waitList = $this->waitList;
38
            $p->waitList[] = $this;
39
            return $p;
40
        }
41
42
        // Return a fulfilled promise and immediately invoke any callbacks.
43
        if ($this->state === self::FULFILLED) {
44
            return $onFulfilled
45
                ? promise_for($this->result)->then($onFulfilled)
46
                : promise_for($this->result);
47
        }
48
49
        // It's either cancelled or rejected, so return a rejected promise
50
        // and immediately invoke any callbacks.
51
        $rejection = rejection_for($this->result);
52
        return $onRejected ? $rejection->then(null, $onRejected) : $rejection;
53
    }
54
55
    public function otherwise(callable $onRejected)
56
    {
57
        return $this->then(null, $onRejected);
58
    }
59
60
    public function wait($unwrap = true)
61
    {
62
        $this->waitIfPending();
63
64
        $inner = $this->result instanceof PromiseInterface
65
            ? $this->result->wait($unwrap)
66
            : $this->result;
67
68
        if ($unwrap) {
69
            if ($this->result instanceof PromiseInterface
70
                || $this->state === self::FULFILLED
71
            ) {
72
                return $inner;
73
            } else {
74
                // It's rejected so "unwrap" and throw an exception.
75
                throw exception_for($inner);
76
            }
77
        }
78
    }
79
80
    public function getState()
81
    {
82
        return $this->state;
83
    }
84
85
    public function cancel()
86
    {
87
        if ($this->state !== self::PENDING) {
88
            return;
89
        }
90
91
        $this->waitFn = $this->waitList = null;
92
93
        if ($this->cancelFn) {
94
            $fn = $this->cancelFn;
95
            $this->cancelFn = null;
96
            try {
97
                $fn();
98
            } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
99
                $this->reject($e);
100
            } catch (\Exception $e) {
101
                $this->reject($e);
102
            }
103
        }
104
105
        // Reject the promise only if it wasn't rejected in a then callback.
106
        if ($this->state === self::PENDING) {
107
            $this->reject(new CancellationException('Promise has been cancelled'));
108
        }
109
    }
110
111
    public function resolve($value)
112
    {
113
        $this->settle(self::FULFILLED, $value);
114
    }
115
116
    public function reject($reason)
117
    {
118
        $this->settle(self::REJECTED, $reason);
119
    }
120
121
    private function settle($state, $value)
122
    {
123
        if ($this->state !== self::PENDING) {
124
            // Ignore calls with the same resolution.
125
            if ($state === $this->state && $value === $this->result) {
126
                return;
127
            }
128
            throw $this->state === $state
129
                ? new \LogicException("The promise is already {$state}.")
130
                : new \LogicException("Cannot change a {$this->state} promise to {$state}");
131
        }
132
133
        if ($value === $this) {
134
            throw new \LogicException('Cannot fulfill or reject a promise with itself');
135
        }
136
137
        // Clear out the state of the promise but stash the handlers.
138
        $this->state = $state;
139
        $this->result = $value;
140
        $handlers = $this->handlers;
141
        $this->handlers = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $handlers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
142
        $this->waitList = $this->waitFn = null;
143
        $this->cancelFn = null;
144
145
        if (!$handlers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $handlers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
146
            return;
147
        }
148
149
        // If the value was not a settled promise or a thenable, then resolve
150
        // it in the task queue using the correct ID.
151
        if (!method_exists($value, 'then')) {
152
            $id = $state === self::FULFILLED ? 1 : 2;
153
            // It's a success, so resolve the handlers in the queue.
154
            queue()->add(static function () use ($id, $value, $handlers) {
155
                foreach ($handlers as $handler) {
156
                    self::callHandler($id, $value, $handler);
157
                }
158
            });
159
        } elseif ($value instanceof Promise
160
            && $value->getState() === self::PENDING
161
        ) {
162
            // We can just merge our handlers onto the next promise.
163
            $value->handlers = array_merge($value->handlers, $handlers);
164
        } else {
165
            // Resolve the handlers when the forwarded promise is resolved.
166
            $value->then(
167
                static function ($value) use ($handlers) {
168
                    foreach ($handlers as $handler) {
169
                        self::callHandler(1, $value, $handler);
170
                    }
171
                },
172
                static function ($reason) use ($handlers) {
173
                    foreach ($handlers as $handler) {
174
                        self::callHandler(2, $reason, $handler);
175
                    }
176
                }
177
            );
178
        }
179
    }
180
181
    /**
182
     * Call a stack of handlers using a specific callback index and value.
183
     *
184
     * @param int   $index   1 (resolve) or 2 (reject).
185
     * @param mixed $value   Value to pass to the callback.
186
     * @param array $handler Array of handler data (promise and callbacks).
187
     *
188
     * @return array Returns the next group to resolve.
189
     */
190
    private static function callHandler($index, $value, array $handler)
191
    {
192
        /** @var PromiseInterface $promise */
193
        $promise = $handler[0];
194
195
        // The promise may have been cancelled or resolved before placing
196
        // this thunk in the queue.
197
        if ($promise->getState() !== self::PENDING) {
198
            return;
199
        }
200
201
        try {
202
            if (isset($handler[$index])) {
203
                $promise->resolve($handler[$index]($value));
204
            } elseif ($index === 1) {
205
                // Forward resolution values as-is.
206
                $promise->resolve($value);
207
            } else {
208
                // Forward rejections down the chain.
209
                $promise->reject($value);
210
            }
211
        } catch (\Throwable $reason) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
212
            $promise->reject($reason);
213
        } catch (\Exception $reason) {
214
            $promise->reject($reason);
215
        }
216
    }
217
218
    private function waitIfPending()
219
    {
220
        if ($this->state !== self::PENDING) {
221
            return;
222
        } elseif ($this->waitFn) {
223
            $this->invokeWaitFn();
224
        } elseif ($this->waitList) {
225
            $this->invokeWaitList();
226
        } else {
227
            // If there's not wait function, then reject the promise.
228
            $this->reject('Cannot wait on a promise that has '
229
                . 'no internal wait function. You must provide a wait '
230
                . 'function when constructing the promise to be able to '
231
                . 'wait on a promise.');
232
        }
233
234
        queue()->run();
235
236
        if ($this->state === self::PENDING) {
237
            $this->reject('Invoking the wait callback did not resolve the promise');
238
        }
239
    }
240
241
    private function invokeWaitFn()
242
    {
243
        try {
244
            $wfn = $this->waitFn;
245
            $this->waitFn = null;
246
            $wfn(true);
247
        } catch (\Exception $reason) {
248
            if ($this->state === self::PENDING) {
249
                // The promise has not been resolved yet, so reject the promise
250
                // with the exception.
251
                $this->reject($reason);
252
            } else {
253
                // The promise was already resolved, so there's a problem in
254
                // the application.
255
                throw $reason;
256
            }
257
        }
258
    }
259
260
    private function invokeWaitList()
261
    {
262
        $waitList = $this->waitList;
263
        $this->waitList = null;
264
265
        foreach ($waitList as $result) {
0 ignored issues
show
Bug introduced by
The expression $waitList of type array<integer,this<Guzzl...\Promise\Promise>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
266
            while (true) {
267
                $result->waitIfPending();
268
269
                if ($result->result instanceof Promise) {
270
                    $result = $result->result;
271
                } else {
272
                    if ($result->result instanceof PromiseInterface) {
273
                        $result->result->wait(false);
274
                    }
275
                    break;
276
                }
277
            }
278
        }
279
    }
280
}
281