Completed
Push — master ( b2dca0...18a7f9 )
by Arul
12:00
created

Async::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace LogicalSteps\Async;
4
5
use Closure;
6
use Error;
7
use Generator;
8
use Psr\Log\LoggerAwareInterface;
9
use Psr\Log\LoggerInterface;
10
use React\EventLoop\LoopInterface;
11
use React\Promise\Deferred;
12
use React\Promise\Promise;
13
use ReflectionGenerator;
14
use Throwable;
15
16
class Async implements LoggerAwareInterface
17
{
18
    const PROMISE_REACT = 'React\Promise\PromiseInterface';
19
    const PROMISE_GUZZLE = 'GuzzleHttp\Promise\PromiseInterface';
20
    const PROMISE_HTTP = 'Http\Promise\Promise';
21
    const PROMISE_AMP = 'Amp\Promise';
22
    /**
23
     * @var LoopInterface
24
     */
25
    protected $loop;
26
    /**
27
     * @var LoggerInterface
28
     */
29
    protected $logger;
30
31
    /**
32
     * @var callable that maps to _execute or _executeReactLoop ...
33
     */
34
    protected $exec;
35
36
    public function __construct()
37
    {
38
        $this->logger = new EchoLogger();
39
        $this->exec = [$this, '_execute'];
40
    }
41
42
    public function promise(Generator $flow): Promise
43
    {
44
        $deferred = new Deferred();
45
        $this->execute($flow, function ($error, $result) use ($deferred) {
46
            if ($error) {
47
                return $deferred->reject($error);
48
            }
49
            $deferred->resolve($result);
50
        });
51
52
        return $deferred->promise();
53
    }
54
55
    public function execute(Generator $flow, callable $callback = null)
56
    {
57
        $this->logger->info('start');
58
        $wrapped_callback = function ($error, $result) use ($callback) {
59
            if ($this->logger) {
60
                $this->logger->info('end');
61
            }
62
            if (is_callable($callback)) {
63
                $callback($error, $result);
64
            }
65
        };
66
        ($this->exec)($flow, $wrapped_callback);
67
68
    }
69
70
    private function _execute(Generator $flow, callable $callback = null, int $depth = 0)
71
    {
72
        $this->_run($flow, $callback, $depth);
73
    }
74
75
    private function _executeReactLoop(Generator $flow, callable $callback = null, int $depth = 0)
76
    {
77
        $this->loop->futureTick(function () use ($flow, $callback, $depth) {
78
            $this->_run($flow, $callback, $depth);
79
        });
80
    }
81
82
    private function _executeAmpLoop(Generator $flow, callable $callback = null, int $depth = 0)
83
    {
84
85
        ('\Amp\Loop::defer')(function () use ($flow, $callback, $depth) {
86
            $this->_run($flow, $callback, $depth);
87
        });
88
    }
89
90
91
    private function _run(Generator $flow, callable $callback = null, int $depth = 0)
92
    {
93
        try {
94
            if (!$flow->valid()) {
95
                $value = $flow->getReturn();
96
                if ($value instanceof Generator) {
97
                    $this->logGenerator($value, $depth);
98
                    ($this->exec)($value, $callback, $depth + 1);
99
                } elseif (is_callable($callback)) {
100
                    $callback(null, $value);
101
                }
102
103
                return;
104
            }
105
            $value = $flow->current();
106
            $args = [];
107
            $func = [];
108
            if (is_array($value) && count($value) > 1) {
109
                $func[] = array_shift($value);
110
                if (is_callable($func[0])) {
111
                    $func = $func[0];
112
                } else {
113
                    $func[] = array_shift($value);
114
                }
115
                $args = $value;
116
            } else {
117
                $func = $value;
118
            }
119
            if (is_callable($func)) {
120
                $this->logCallable($func, $args, $depth);
121 View Code Duplication
                $args[] = function ($error, $result) use ($flow, $callback, $depth) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
                    if ($error) {
123
                        if ($this->logger) {
124
                            $this->logger->error((string)$error, compact('depth'));
125
                        }
126
                        if (is_callable($callback)) {
127
                            $callback($error);
128
                        }
129
130
                        return;
131
                    }
132
                    $flow->send($result);
133
                    ($this->exec)($flow, $callback, $depth);
134
                };
135
                call_user_func_array($func, $args);
136
            } elseif ($value instanceof Generator) {
137
                $this->logGenerator($value, $depth);
138 View Code Duplication
                ($this->exec)($value, function ($error, $result) use ($flow, $callback, $depth) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
139
                    if ($error) {
140
                        if ($this->logger) {
141
                            $this->logger->error((string)$error);
142
                        }
143
                        if (is_callable($callback)) {
144
                            $callback($error);
145
                        }
146
147
                        return;
148
                    }
149
                    $flow->send($result);
150
                    ($this->exec)($flow, $callback, $depth);
151
                }, $depth + 1);
152
            } elseif (is_a($value, static::PROMISE_REACT)) {
153
                $this->handlePromise($flow, $callback, $depth, $value);
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 91 can also be of type null; however, LogicalSteps\Async\Async::handlePromise() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
154
            } elseif (is_a($value, static::PROMISE_GUZZLE) || is_a($value, static::PROMISE_HTTP)) {
155
                $this->handlePromise($flow, $callback, $depth, $value);
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 91 can also be of type null; however, LogicalSteps\Async\Async::handlePromise() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
156
                $value->wait(false);
157
            } elseif (is_a($value, static::PROMISE_AMP)) {
158
                if ($this->logger) {
159
                    $this->logger->info('await $promise;');
160
                }
161
                $value->onResolve(
162 View Code Duplication
                    function ($error, $result) use ($flow, $callback, $depth) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
163
                        if ($error) {
164
                            if ($this->logger) {
165
                                $this->logger->error((string)$error);
166
                            }
167
                            if (is_callable($callback)) {
168
                                $callback($error);
169
                            }
170
171
                            return;
172
                        }
173
                        $flow->send($result);
174
                        ($this->exec)($flow, $callback, $depth);
175
                    }
176
                );
177
            } else {
178
                $flow->send($value);
179
                ($this->exec)($flow, $callback, $depth);
180
            }
181
182
        } catch
183
        (Throwable $t) {
184
            $flow->throw($t);
185
            ($this->exec)($flow);
186
        }
187
    }
188
189
    /**
190
     * Handle known promise interfaces
191
     *
192
     * @param Generator $flow
193
     * @param callable $callback
194
     * @param int $depth
195
     * @param \React\Promise\PromiseInterface|\GuzzleHttp\Promise\PromiseInterface $value
196
     */
197
    private function handlePromise(Generator $flow, callable $callback, int $depth, $value)
198
    {
199
        if ($this->logger) {
200
            $this->logger->info('await $promise;');
201
        }
202
        $value->then(
203
            function ($result) use ($flow, $callback, $depth) {
204
                $flow->send($result);
205
                ($this->exec)($flow, $callback, $depth);
206
            },
207
            function ($error) use ($callback, $depth) {
208
                if ($this->logger) {
209
                    $this->logger->error((string)$error, compact('depth'));
210
                }
211
                if (is_callable($callback)) {
212
                    $callback($error);
213
                }
214
            }
215
        );
216
    }
217
218
    private function logCallable(callable $callable, array $arguments, int $depth = 0)
219
    {
220
        if (!$this->logger) {
221
            return;
222
        }
223
        if (is_array($callable)) {
224
            $name = $callable[0];
225
            if (is_object($name)) {
226
                $name = '$' . lcfirst(get_class($name)) . '->' . $callable[1];
227
            } else {
228
                $name .= '::' . $callable[1];
229
            }
230
231
        } else {
232
            if (is_string($callable)) {
233
                $name = $callable;
234
            } elseif ($callable instanceof Closure) {
235
                $name = 'closure';
236
            } else {
237
                $name = 'callable';
238
            }
239
        }
240
        $this->logger->info('await ' . $name . $this->format($arguments), compact('depth'));
241
242
    }
243
244
    private function logGenerator(Generator $generator, int $depth = 0)
245
    {
246
        if (!$generator->valid() || !$this->logger) {
247
            return;
248
        }
249
        $info = new ReflectionGenerator($generator);
250
        $f = $info->getFunction();
251
        if ($name = $info->getThis()) {
252
            if (is_object($name)) {
253
                $name = '$' . lcfirst(get_class($name)) . '->' . $f->name;
254
            } else {
255
                $name .= '::' . $f->name;
256
            }
257
        } else {
258
            $name = $f->name;
259
        }
260
        $args = [];
261
        foreach ($f->getParameters() as $parameter) {
262
            $args[] = '$' . $parameter->name;
263
        }
264
        $this->logger->info('await ' . $name . '(' . implode(', ', $args) . ');', compact('depth'));
265
266
    }
267
268
    private function format($parameters)
269
    {
270
        return '(' . substr(json_encode($parameters), 1, -1) . ');';
271
    }
272
273
    /**
274
     * A method used to test whether this class is autoloaded.
275
     *
276
     * @return bool
277
     *
278
     * @see \LogicalSteps\Async\Test\DummyTest
279
     */
280
    public function autoloaded()
281
    {
282
        return true;
283
    }
284
285
    /**
286
     * Sets a logger instance on the object.
287
     *
288
     * @param LoggerInterface $logger
289
     *
290
     * @return void
291
     */
292
    public function setLogger(LoggerInterface $logger)
293
    {
294
        $this->logger = $logger;
295
    }
296
297
    /**
298
     * @param LoopInterface $loop
299
     */
300
    public function setLoop(LoopInterface $loop)
301
    {
302
        $this->loop = $loop;
303
        $this->exec = [$this, '_executeReactLoop'];
304
    }
305
306
    public function useAmpLoop()
307
    {
308
        $this->exec = [$this, '_executeAmpLoop'];
309
    }
310
}
311