Async::_handleCallback()   B
last analyzed

Complexity

Conditions 9
Paths 33

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 9
eloc 18
c 3
b 0
f 1
nc 33
nop 4
dl 0
loc 25
rs 8.0555
1
<?php
2
3
namespace LogicalSteps\Async;
4
5
6
use Amp\Loop\Driver;
7
use Closure;
8
use Exception;
9
use Generator;
10
use Psr\Log\LoggerInterface;
11
use React\EventLoop\LoopInterface;
12
use React\Promise\Promise;
13
use React\Promise\PromiseInterface;
14
use ReflectionException;
15
use ReflectionFunction;
16
use ReflectionFunctionAbstract;
17
use ReflectionGenerator;
18
use ReflectionMethod;
19
use ReflectionObject;
20
use Throwable;
21
use TypeError;
22
use UnexpectedValueException;
23
24
use function GuzzleHttp\Promise\all as guzzleAll;
25
26
/**
27
 * @method static mixed wait($process) synchronously wait for the completion of an asynchronous process
28
 * @method mixed wait($process) synchronously wait for the completion of an asynchronous process
29
 *
30
 * @method static PromiseInterface await($process) await for the completion of an asynchronous process
31
 * @method PromiseInterface await($process) await for the completion of an asynchronous process
32
 *
33
 * @method static void await($process, callable $callback) await for the completion of an asynchronous process
34
 * @method void await($process, callable $callback) await for the completion of an asynchronous process
35
 *
36
 * @method static PromiseInterface awaitAll(array $processes) concurrently await for multiple processes
37
 * @method PromiseInterface awaitAll(array $processes) concurrently await for multiple processes
38
 *
39
 * @method static void awaitAll(array $processes, callable $callback) concurrently await for multiple processes
40
 * @method void awaitAll(array $processes, callable $callback) concurrently await for multiple processes
41
 *
42
 * @method static setLogger(LoggerInterface|null $logger)
43
 * @method setLogger(LoggerInterface|null $logger)
44
 *
45
 * @method static setEventLoop(LoopInterface|Driver|null $loop)
46
 * @method setEventLoop(LoopInterface|Driver|null $loop)
47
 */
48
class Async
49
{
50
    public const PROMISE_REACT = 'React\Promise\PromiseInterface';
51
    public const PROMISE_AMP = 'Amp\Promise';
52
    public const PROMISE_GUZZLE = 'GuzzleHttp\Promise\PromiseInterface';
53
    public const PROMISE_HTTP = 'Http\Promise\Promise';
54
55
    /** @var string action to return a promise instead of awaiting the response of the process. */
56
    public const promise = 'promise';
57
    /** @var string action to run current process side by side with the remainder of the process. */
58
    public const parallel = 'parallel';
59
    /** @var string action to await for all parallel processes previously to finish. */
60
    public const all = 'all';
61
    /** @var string action to await for current  processes to finish. this is the default action. */
62
    public const await = 'await';
63
    /** @var string action to run current process after finished executing the function. */
64
    public const later = 'later';
65
66
    public const ACTIONS = [self::await, self::parallel, self::all, self::promise, self::later];
67
68
    public static $knownPromises = [
69
        self::PROMISE_REACT,
70
        self::PROMISE_AMP,
71
        self::PROMISE_GUZZLE,
72
        self::PROMISE_HTTP,
73
    ];
74
75
    /**
76
     * @var LoggerInterface|null
77
     */
78
    protected $logger;
79
80
    /**
81
     * @var LoopInterface|Driver|null;
82
     */
83
    protected $loop;
84
85
86
    /**
87
     * @var bool
88
     */
89
    public $waitForGuzzleAndHttplug = true;
90
91
    /**
92
     * @var bool
93
     */
94
    protected $parallelGuzzleLoading = false;
95
96
    protected $guzzlePromises = [];
97
98
    /**
99
     * Async constructor.
100
     * @param LoggerInterface|null $logger
101
     * @param LoopInterface|Driver|null $eventLoop optional. required when using reactphp or amphp
102
     */
103
    public function __construct(?LoggerInterface $logger = null, $eventLoop = null)
104
    {
105
        $this->logger = $logger;
106
        $this->_setEventLoop($eventLoop);
107
    }
108
109
    public function __call($name, $arguments)
110
    {
111
        $value = null;
112
        $method = "_$name";
113
        switch ($name) {
114
            case 'await':
115
            case 'awaitAll':
116
                if ($this->logger) {
117
                    $this->logger->info('start');
118
                }
119
                if (1 == count($arguments)) {
120
                    //return promise
121
                    list($value, $resolver, $rejector) = $this->makePromise();
122
                    $callback = function ($error = null, $result = null) use ($resolver, $rejector) {
123
                        if ($this->logger) {
124
                            $this->logger->info('end');
125
                        }
126
                        if ($error) {
127
                            return $rejector($error);
128
                        }
129
                        return $resolver($result);
130
                    };
131
                    $arguments[1] = $callback;
132
                } elseif ($this->logger) {
133
                    $c = $arguments[1];
134
                    $callback = function ($error, $result = null) use ($c) {
135
                        if ($this->logger) {
136
                            $this->logger->info('end');
137
                        }
138
                        $c($error, $result);
139
                    };
140
                    $arguments[1] = $callback;
141
                }
142
                if ('await' == $name) {
143
                    $arguments[2] = -1;//depth
144
                    $method = '_handle';
145
                }
146
                break;
147
            case 'setLogger':
148
            case 'setEventLoop':
149
                break;
150
            case 'wait':
151
                return call_user_func_array([$this, $method], $arguments);
152
            default:
153
                return null;
154
        }
155
        call_user_func_array([$this, $method], $arguments);
156
        return $value;
157
    }
158
159
    public static function __callStatic($name, $arguments)
160
    {
161
        if (empty($arguments) && in_array($name, self::ACTIONS)) {
162
            return $name;
163
        }
164
        static $instance;
165
        if (!$instance) {
166
            $instance = new static();
167
        }
168
        return $instance->__call($name, $arguments);
169
    }
170
171
    /**
172
     * Throws specified or subclasses of specified exception inside the generator class so that it can be handled.
173
     *
174
     * @param string $throwable
175
     * @return string command
176
     *
177
     * @throws TypeError when given value is not a valid exception
178
     */
179
    public static function throw(string $throwable): string
180
    {
181
        if (is_a($throwable, Throwable::class, true)) {
182
            return __FUNCTION__ . ':' . $throwable;
183
        }
184
        throw new TypeError('Invalid value for throwable, it must extend Throwable class');
185
    }
186
187
    protected function _awaitAll(array $processes, callable $callback): void
188
    {
189
        $this->parallelGuzzleLoading = true;
190
        $results = [];
191
        $failed = false;
192
        foreach ($processes as $key => $process) {
193
            if ($failed) {
194
                break;
195
            }
196
            $c = function ($error = null, $result = null) use ($key, &$results, $processes, $callback, &$failed) {
197
                if ($failed) {
198
                    return;
199
                }
200
                if ($error) {
201
                    $failed = true;
202
                    $callback($error);
203
                    return;
204
                }
205
                $results[$key] = $result;
206
                if (count($results) == count($processes)) {
207
                    $callback(null, $results);
208
                }
209
            };
210
            $this->_handle($process, $c, -1);
211
        }
212
        if (!empty($this->guzzlePromises)) {
213
            guzzleAll($this->guzzlePromises)->wait(false);
214
            $this->guzzlePromises = [];
215
        }
216
        $this->parallelGuzzleLoading = false;
217
    }
218
219
    /**
220
     * Sets a logger instance on the object.
221
     *
222
     * @param LoggerInterface|null $logger
223
     *
224
     * @return void
225
     */
226
    protected function _setLogger(?LoggerInterface $logger)
227
    {
228
        $this->logger = $logger;
229
    }
230
231
    /**
232
     * Sets a logger instance on the object.
233
     *
234
     * @param LoopInterface|Driver|null $loop
235
     *
236
     * @return void
237
     */
238
    protected function _setEventLoop($loop)
239
    {
240
        if ($loop && !($loop instanceof LoopInterface || $loop instanceof Driver)) {
0 ignored issues
show
introduced by
$loop is always a sub-type of Amp\Loop\Driver.
Loading history...
241
            throw new TypeError(
242
                'Argument 1 passed to LogicalSteps/Async/Async::_setEventLoop() must be ' .
243
                'an instance of React\EventLoop\LoopInterface or use Amp\Loop\Driver or null.'
244
            );
245
        }
246
        $this->loop = $loop;
247
    }
248
249
    private function makePromise()
250
    {
251
        $resolver = $rejector = null;
252
        $promise = new Promise(
253
            function ($resolve, $reject, $notify) use (&$resolver, &$rejector) {
254
                $resolver = $resolve;
255
                $rejector = $reject;
256
            }
257
        );
258
        return [$promise, $resolver, $rejector];
259
    }
260
261
    protected function _wait($process)
262
    {
263
        if ($this->logger) {
264
            $this->logger->info('start');
265
        }
266
        $waiting = true;
267
        $result = null;
268
        $exception = null;
269
        $isRejected = false;
270
271
        $callback = function ($error = null, $r = null) use (&$result, &$waiting, &$isRejected, &$exception) {
272
            $waiting = false;
273
            if ($this->loop) {
274
                $this->loop->stop();
275
            }
276
            if ($error) {
277
                $isRejected = true;
278
                $exception = $error;
279
                return;
280
            }
281
            $result = $r;
282
        };
283
        $this->_handle($process, $callback, -1);
284
        while ($waiting) {
285
            if ($this->loop) {
286
                $this->loop->run();
287
            }
288
        }
289
        if ($this->logger) {
290
            $this->logger->info('end');
291
        }
292
        if ($isRejected) {
293
            if (!$exception instanceof Exception) {
294
                $exception = new UnexpectedValueException(
295
                    'process failed with ' . (is_object($exception) ? get_class($exception) : gettype($exception))
296
                );
297
            }
298
            throw $exception;
299
        }
300
301
        return $result;
302
    }
303
304
    protected function _handle($process, callable $callback, int $depth = 0): void
305
    {
306
        $arguments = [];
307
        $func = [];
308
        if (is_array($process) && count($process) > 1) {
309
            $copy = $process;
310
            $func[] = array_shift($copy);
311
            if (is_callable($func[0])) {
312
                $func = $func[0];
313
            } else {
314
                $func[] = array_shift($copy);
315
            }
316
            $arguments = $copy;
317
        } else {
318
            $func = $process;
319
        }
320
        if (is_callable($func)) {
321
            $this->_handleCallback($func, $arguments, $callback, $depth);
322
        } elseif ($process instanceof Generator) {
323
            $this->_handleGenerator($process, $callback, 1 + $depth);
324
        } elseif (is_object($process) && $implements = array_intersect(
325
                class_implements($process),
326
                Async::$knownPromises
327
            )) {
328
            $this->_handlePromise($process, array_shift($implements), $callback, $depth);
329
        } else {
330
            $callback(null, $process);
331
        }
332
    }
333
334
335
    protected function _handleCallback(callable $callable, array $parameters, callable $callback, int $depth = 0)
336
    {
337
        $this->logCallback($callable, $parameters, $depth);
338
        try {
339
            if (is_array($callable)) {
340
                $rf = new ReflectionMethod($callable[0], $callable[1]);
341
            } elseif (is_string($callable)) {
342
                $rf = new ReflectionFunction($callable);
343
            } elseif (is_a($callable, 'Closure') || is_callable($callable, '__invoke')) {
0 ignored issues
show
Bug introduced by
$callable of type callable is incompatible with the type object|string expected by parameter $object of is_a(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

343
            } elseif (is_a(/** @scrutinizer ignore-type */ $callable, 'Closure') || is_callable($callable, '__invoke')) {
Loading history...
Bug introduced by
'__invoke' of type string is incompatible with the type boolean expected by parameter $syntax_only of is_callable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

343
            } elseif (is_a($callable, 'Closure') || is_callable($callable, /** @scrutinizer ignore-type */ '__invoke')) {
Loading history...
344
                $ro = new ReflectionObject($callable);
0 ignored issues
show
Bug introduced by
$callable of type callable is incompatible with the type object expected by parameter $argument of ReflectionObject::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

344
                $ro = new ReflectionObject(/** @scrutinizer ignore-type */ $callable);
Loading history...
345
                $rf = $ro->getMethod('__invoke');
346
            }
347
            $current = count($parameters);
348
            $total = $rf->getNumberOfParameters();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rf does not seem to be defined for all execution paths leading up to this point.
Loading history...
349
            $ps = $rf->getParameters();
350
            if ($current + 1 < $total) {
351
                for ($i = $current; $i < $total - 1; $i++) {
352
                    $parameters[$i] = $ps[$i]->isDefaultValueAvailable() ? $ps[$i]->getDefaultValue() : null;
353
                }
354
            }
355
        } catch (ReflectionException $e) {
356
            //ignore
357
        }
358
        $parameters[] = $callback;
359
        call_user_func_array($callable, $parameters);
360
    }
361
362
    protected function _handleGenerator(Generator $flow, callable $callback, int $depth = 0)
363
    {
364
        $this->logGenerator($flow, $depth - 1);
365
        try {
366
            if (!$flow->valid()) {
367
                $callback(null, $flow->getReturn());
368
                if (!empty($flow->later)) {
369
                    $this->_awaitAll(
370
                        $flow->later,
0 ignored issues
show
Bug introduced by
The property later does not seem to exist on Generator.
Loading history...
371
                        function ($error = null, $results = null) {
372
                        }
373
                    );
374
                    unset($flow->later);
375
                }
376
                return;
377
            }
378
            $value = $flow->current();
379
            $actions = $this->parse($flow->key() ?: Async::await);
380
            $next = function ($error = null, $result = null) use ($flow, $actions, $callback, $depth) {
381
                $value = $error ?: $result;
382
                if ($value instanceof Throwable) {
383
                    if (isset($actions['throw']) && is_a($value, $actions['throw'])) {
384
                        /** @scrutinizer ignore-call */
385
                        $flow->throw($value);
386
                        $this->_handleGenerator($flow, $callback, $depth);
387
                        return;
388
                    }
389
                    $callback($value, null);
390
                    return;
391
                }
392
                $flow->send($value);
393
                $this->_handleGenerator($flow, $callback, $depth);
394
            };
395
            if (key_exists(self::later, $actions)) {
396
                if (!isset($flow->later)) {
397
                    $flow->later = [];
398
                }
399
                if ($this->logger) {
400
                    $this->logger->info('later task scheduled', compact('depth'));
401
                }
402
                $flow->later[] = $value;
403
                return $next(null, $value);
404
            }
405
            if (key_exists(self::parallel, $actions)) {
406
                if (!isset($flow->parallel)) {
407
                    $flow->parallel = [];
0 ignored issues
show
Bug introduced by
The property parallel does not seem to exist on Generator.
Loading history...
408
                }
409
                $flow->parallel[] = $value;
410
                if (!isset($this->action)) {
411
                    $this->action = [];
0 ignored issues
show
Bug Best Practice introduced by
The property action does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
412
                }
413
                $this->action[] = self::parallel;
414
                return $next(null, $value);
415
            }
416
            if (key_exists(self::all, $actions)) {
417
                $tasks = Async::parallel === $value && isset($flow->parallel) ? $flow->parallel : $value;
418
                unset($flow->parallel);
419
                if (is_array($tasks) && count($tasks)) {
420
                    if ($this->logger) {
421
                        $this->logger->info(
422
                            sprintf("all {%d} tasks awaited.", count($tasks)),
423
                            compact('depth')
424
                        );
425
                    }
426
                    return $this->_awaitAll($tasks, $next);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->_awaitAll($tasks, $next) targeting LogicalSteps\Async\Async::_awaitAll() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
427
                }
428
                return $next(null, []);
429
            }
430
            $this->_handle($value, $next, $depth);
431
        } catch (Throwable $throwable) {
432
            $callback($throwable, null);
433
        }
434
    }
435
436
    /**
437
     * Handle known promise interfaces
438
     *
439
     * @param PromiseInterface|\GuzzleHttp\Promise\PromiseInterface|\Amp\Promise|\Http\Promise\Promise $knownPromise
440
     * @param string $interface
441
     * @param callable $callback
442
     * @param int $depth
443
     * @return void
444
     */
445
    protected function _handlePromise($knownPromise, string $interface, callable $callback, int $depth = 0)
446
    {
447
        $this->logPromise($knownPromise, $interface, $depth);
448
        $resolver = function ($result) use ($callback) {
449
            $callback(null, $result);
450
        };
451
        $rejector = function ($error) use ($callback) {
452
            $callback($error, null);
453
        };
454
        try {
455
            switch ($interface) {
456
                case static::PROMISE_REACT:
457
                    $knownPromise->then($resolver, $rejector);
0 ignored issues
show
Bug introduced by
The method then() does not exist on Amp\Promise. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

457
                    $knownPromise->/** @scrutinizer ignore-call */ 
458
                                   then($resolver, $rejector);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
458
                    break;
459
                case static::PROMISE_GUZZLE:
460
                    $knownPromise->then($resolver, $rejector);
461
                    if ($this->waitForGuzzleAndHttplug) {
462
                        if ($this->parallelGuzzleLoading) {
463
                            $this->guzzlePromises[] = $knownPromise;
464
                        } else {
465
                            $knownPromise->wait(false);
0 ignored issues
show
Bug introduced by
The method wait() does not exist on React\Promise\PromiseInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

465
                            $knownPromise->/** @scrutinizer ignore-call */ 
466
                                           wait(false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method wait() does not exist on Amp\Promise. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

465
                            $knownPromise->/** @scrutinizer ignore-call */ 
466
                                           wait(false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
466
                        }
467
                    }
468
                    break;
469
                case static::PROMISE_HTTP:
470
                    $knownPromise->then($resolver, $rejector);
471
                    if ($this->waitForGuzzleAndHttplug) {
472
                        $knownPromise->wait(false);
473
                    }
474
                    break;
475
                case static::PROMISE_AMP:
476
                    $knownPromise->onResolve(
0 ignored issues
show
Bug introduced by
The method onResolve() does not exist on GuzzleHttp\Promise\PromiseInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

476
                    $knownPromise->/** @scrutinizer ignore-call */ 
477
                                   onResolve(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method onResolve() does not exist on Http\Promise\Promise. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

476
                    $knownPromise->/** @scrutinizer ignore-call */ 
477
                                   onResolve(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method onResolve() does not exist on React\Promise\PromiseInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

476
                    $knownPromise->/** @scrutinizer ignore-call */ 
477
                                   onResolve(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
477
                        function ($error = null, $result = null) use ($resolver, $rejector) {
478
                            $error ? $rejector($error) : $resolver($result);
479
                        }
480
                    );
481
                    break;
482
            }
483
        } catch (Exception $e) {
484
            $rejector($e);
485
        }
486
    }
487
488
    private function parse(string $command): array
489
    {
490
        $arr = [];
491
        if (strlen($command)) {
492
            parse_str(str_replace(['|', ':'], ['&', '='], $command), $arr);
493
        }
494
        return $arr;
495
    }
496
497
    private function action()
498
    {
499
        if (!empty($this->action)) {
500
            return array_shift($this->action);
501
        }
502
        return self::await;
503
    }
504
505
    private function logCallback(callable $callable, array $parameters, int $depth = 0)
506
    {
507
        if ($depth < 0 || !$this->logger) {
508
            return;
509
        }
510
        if (is_array($callable)) {
511
            $name = $callable[0];
512
            if (is_object($name)) {
513
                $name = '$' . lcfirst(get_class($name)) . '->' . $callable[1];
514
            } else {
515
                $name .= '::' . $callable[1];
516
            }
517
        } elseif (is_string($callable)) {
518
            $name = $callable;
519
        } elseif ($callable instanceof Closure) {
520
            $name = '$closure';
521
        } else {
522
            $name = '$callable';
523
        }
524
        $this->logger->info(
525
            sprintf("%s %s%s", $this->action(), $name, $this->format($parameters)),
526
            compact('depth')
527
        );
528
    }
529
530
    private function logPromise(/** @scrutinizer ignore-unused */ $promise, string $interface, int $depth)
531
    {
532
        if ($depth < 0 || !$this->logger) {
533
            return;
534
        }
535
        $type = 'unknown';
536
        switch ($interface) {
537
            case static::PROMISE_REACT:
538
                $type = 'react';
539
                break;
540
            case static::PROMISE_GUZZLE:
541
                $type = 'guzzle';
542
                break;
543
            case static::PROMISE_HTTP:
544
                $type = 'httplug';
545
                break;
546
            case static::PROMISE_AMP:
547
                $type = 'amp';
548
                break;
549
        }
550
        $this->logger->info(
551
            sprintf("%s \$%sPromise;", $this->action(), $type),
552
            compact('depth')
553
        );
554
    }
555
556
    private function logGenerator(Generator $generator, int $depth = 0)
557
    {
558
        if ($depth < 0 || !$generator->valid() || !$this->logger) {
559
            return;
560
        }
561
        $info = new ReflectionGenerator($generator);
562
        $this->logReflectionFunction($info->getFunction(), $depth);
563
    }
564
565
    private function format($parameters)
566
    {
567
        return '(' . substr(json_encode($parameters), 1, -1) . ');';
568
    }
569
570
    private function logReflectionFunction(ReflectionFunctionAbstract $function, int $depth = 0)
571
    {
572
        if ($function instanceof ReflectionMethod) {
573
            $name = $function->getDeclaringClass()->getShortName();
574
            if ($function->isStatic()) {
575
                $name .= '::' . $function->name;
576
            } else {
577
                $name = '$' . lcfirst($name) . '->' . $function->name;
578
            }
579
        } elseif ($function->isClosure()) {
580
            $name = '$closure';
581
        } else {
582
            $name = $function->name;
583
        }
584
        $args = [];
585
        foreach ($function->getParameters() as $parameter) {
586
            $args[] = '$' . $parameter->name;
587
        }
588
        $this->logger->info(
0 ignored issues
show
Bug introduced by
The method info() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

588
        $this->logger->/** @scrutinizer ignore-call */ 
589
                       info(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
589
            sprintf("%s %s(%s);", $this->action(), $name, implode(', ', $args)),
590
            compact('depth')
591
        );
592
    }
593
}
594