Passed
Push — master ( 931c2d...6f92a7 )
by Arul
11:37
created

Async   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 501
Duplicated Lines 0 %

Importance

Changes 44
Bugs 3 Features 3
Metric Value
eloc 300
c 44
b 3
f 3
dl 0
loc 501
rs 2
wmc 112

19 Methods

Rating   Name   Duplication   Size   Complexity  
B __call() 0 43 10
B _awaitAll() 0 28 7
A action() 0 6 2
A logGenerator() 0 7 4
B _handleCallback() 0 25 9
A _setLogger() 0 3 1
A __callStatic() 0 10 4
B logCallback() 0 25 7
A makePromise() 0 8 1
A parse() 0 7 2
B _handle() 0 25 8
A __construct() 0 4 2
A throw() 0 6 2
B _handlePromise() 0 39 10
A format() 0 3 1
B handleCommands() 0 40 9
A logReflectionFunction() 0 21 5
B logPromise() 0 23 7
D _handleGenerator() 0 68 21

How to fix   Complexity   

Complex Class

Complex classes like Async 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.

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 Async, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace LogicalSteps\Async;
4
5
6
use Closure;
7
use Generator;
8
use Psr\Log\LoggerInterface;
9
use React\Promise\Promise;
10
use React\Promise\PromiseInterface;
11
use ReflectionException;
12
use ReflectionFunction;
13
use ReflectionFunctionAbstract;
14
use ReflectionGenerator;
15
use ReflectionMethod;
16
use ReflectionObject;
17
use Throwable;
18
use TypeError;
19
use function GuzzleHttp\Promise\all as guzzleAll;
20
21
/**
22
 * @method static PromiseInterface await($process) await for the completion of an asynchronous process
23
 * @method PromiseInterface await($process) await for the completion of an asynchronous process
24
 *
25
 * @method static void await($process, callable $callback) await for the completion of an asynchronous process
26
 * @method void await($process, callable $callback) await for the completion of an asynchronous process
27
 *
28
 * @method static PromiseInterface awaitAll(array $processes) concurrently await for multiple processes
29
 * @method PromiseInterface awaitAll(array $processes) concurrently await for multiple processes
30
 *
31
 * @method static void awaitAll(array $processes, callable $callback) concurrently await for multiple processes
32
 * @method void awaitAll(array $processes, callable $callback) concurrently await for multiple processes
33
 *
34
 * @method static setLogger(LoggerInterface $param)
35
 * @method setLogger(LoggerInterface $param)
36
 */
37
class Async
38
{
39
    const PROMISE_REACT = 'React\Promise\PromiseInterface';
40
    const PROMISE_AMP = 'Amp\Promise';
41
    const PROMISE_GUZZLE = 'GuzzleHttp\Promise\PromiseInterface';
42
    const PROMISE_HTTP = 'Http\Promise\Promise';
43
44
    /** @var string action to return a promise instead of awaiting the response of the process. */
45
    const promise = 'promise';
46
    /** @var string action to run current process side by side with the remainder of the process. */
47
    const parallel = 'parallel';
48
    /** @var string action to await for all parallel processes previously to finish. */
49
    const all = 'all';
50
    /** @var string action to await for current  processes to finish. this is the default action. */
51
    const await = 'await';
52
    /** @var string action to run current process after finished executing the function. */
53
    const later = 'later';
54
55
    const ACTIONS = [self::await, self::parallel, self::all, self::promise, self::later];
56
57
    public static $knownPromises = [
58
        self::PROMISE_REACT,
59
        self::PROMISE_AMP,
60
        self::PROMISE_GUZZLE,
61
        self::PROMISE_HTTP,
62
    ];
63
64
    /**
65
     * @var LoggerInterface
66
     */
67
    protected $logger;
68
    /**
69
     * @var bool
70
     */
71
    public $waitForGuzzleAndHttplug = true;
72
73
    /**
74
     * @var bool
75
     */
76
    protected $parallelGuzzleLoading = false;
77
78
    protected $guzzlePromises = [];
79
80
    public function __construct(LoggerInterface $logger = null)
81
    {
82
        if ($logger) {
83
            $this->logger = $logger;
84
        }
85
    }
86
87
    public function __call($name, $arguments)
88
    {
89
        $value = null;
90
        $method = "_$name";
91
        switch ($name) {
92
            case 'await':
93
            case 'awaitAll':
94
                if ($this->logger) {
95
                    $this->logger->info('start');
96
                }
97
                if (1 == count($arguments)) {
98
                    //return promise
99
                    list($value, $resolver, $rejector) = $this->makePromise();
100
                    $callback = function ($error = null, $result = null) use ($resolver, $rejector) {
101
                        if ($this->logger) {
102
                            $this->logger->info('end');
103
                        }
104
                        if ($error) {
105
                            return $rejector($error);
106
                        }
107
                        return $resolver($result);
108
                    };
109
                    $arguments[1] = $callback;
110
                } elseif ($this->logger) {
111
                    $c = $arguments[1];
112
                    $callback = function ($error, $result = null) use ($c) {
113
                        $this->logger->info('end');
114
                        $c($error, $result);
115
                    };
116
                    $arguments[1] = $callback;
117
                }
118
                if ('await' == $name) {
119
                    $arguments[2] = -1;//depth
120
                    $method = '_handle';
121
                }
122
                break;
123
            case 'setLogger':
124
                break;
125
            default:
126
                return null;
127
        }
128
        call_user_func_array([$this, $method], $arguments);
129
        return $value;
130
    }
131
132
    public static function __callStatic($name, $arguments)
133
    {
134
        if (empty($arguments) && in_array($name, self::ACTIONS)) {
135
            return $name;
136
        }
137
        static $instance;
138
        if (!$instance) {
139
            $instance = new static();
140
        }
141
        return $instance->__call($name, $arguments);
142
    }
143
144
    /**
145
     * Throws specified or subclasses of specified exception inside the generator class so that it can be handled.
146
     *
147
     * @param string $throwable
148
     * @return string command
149
     *
150
     * @throws TypeError when given value is not a valid exception
151
     */
152
    public static function throw(string $throwable): string
153
    {
154
        if (is_a($throwable, Throwable::class, true)) {
155
            return __FUNCTION__ . ':' . $throwable;
156
        }
157
        throw new TypeError('Invalid value for throwable, it must extend Throwable class');
158
    }
159
160
    protected function _awaitAll(array $processes, callable $callback): void
161
    {
162
        $this->parallelGuzzleLoading = true;
163
        $results = [];
164
        $failed = false;
165
        foreach ($processes as $key => $process) {
166
            if ($failed)
167
                break;
168
            $c = function ($error = null, $result = null) use ($key, &$results, $processes, $callback, &$failed) {
169
                if ($failed)
170
                    return;
171
                if ($error) {
172
                    $failed = true;
173
                    $callback($error);
174
                    return;
175
                }
176
                $results[$key] = $result;
177
                if (count($results) == count($processes)) {
178
                    $callback(null, $results);
179
                }
180
            };
181
            $this->_handle($process, $c, -1);
182
        }
183
        if (!empty($this->guzzlePromises)) {
184
            guzzleAll($this->guzzlePromises)->wait(false);
185
            $this->guzzlePromises = [];
186
        }
187
        $this->parallelGuzzleLoading = false;
188
    }
189
190
    /**
191
     * Sets a logger instance on the object.
192
     *
193
     * @param LoggerInterface $logger
194
     *
195
     * @return void
196
     */
197
    protected function _setLogger(LoggerInterface $logger)
198
    {
199
        $this->logger = $logger;
200
    }
201
202
    private function makePromise()
203
    {
204
        $resolver = $rejector = null;
205
        $promise = new Promise(function ($resolve, $reject, $notify) use (&$resolver, &$rejector) {
206
            $resolver = $resolve;
207
            $rejector = $reject;
208
        });
209
        return [$promise, $resolver, $rejector];
210
    }
211
212
    protected function _handle($process, callable $callback, int $depth = 0): void
213
    {
214
        $arguments = [];
215
        $func = [];
216
        if (is_array($process) && count($process) > 1) {
217
            $copy = $process;
218
            $func[] = array_shift($copy);
219
            if (is_callable($func[0])) {
220
                $func = $func[0];
221
            } else {
222
                $func[] = array_shift($copy);
223
            }
224
            $arguments = $copy;
225
        } else {
226
            $func = $process;
227
        }
228
        if (is_callable($func)) {
229
            $this->_handleCallback($func, $arguments, $callback, $depth);
230
        } elseif ($process instanceof Generator) {
231
            $this->_handleGenerator($process, $callback, 1 + $depth);
232
        } elseif (is_object($process) && $implements = array_intersect(class_implements($process),
233
                Async::$knownPromises)) {
234
            $this->_handlePromise($process, array_shift($implements), $callback, $depth);
235
        } else {
236
            $callback(null, $process);
237
        }
238
    }
239
240
241
    protected function _handleCallback(callable $callable, array $parameters, callable $callback, int $depth = 0)
242
    {
243
        $this->logCallback($callable, $parameters, $depth);
244
        try {
245
            if (is_array($callable)) {
246
                $rf = new ReflectionMethod($callable[0], $callable[1]);
247
            } elseif (is_string($callable)) {
248
                $rf = new ReflectionFunction($callable);
249
            } elseif (is_a($callable, 'Closure') || is_callable($callable, '__invoke')) {
0 ignored issues
show
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

249
            } elseif (is_a($callable, 'Closure') || is_callable($callable, /** @scrutinizer ignore-type */ '__invoke')) {
Loading history...
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

249
            } elseif (is_a(/** @scrutinizer ignore-type */ $callable, 'Closure') || is_callable($callable, '__invoke')) {
Loading history...
250
                $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

250
                $ro = new ReflectionObject(/** @scrutinizer ignore-type */ $callable);
Loading history...
251
                $rf = $ro->getMethod('__invoke');
252
            }
253
            $current = count($parameters);
254
            $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...
255
            $ps = $rf->getParameters();
256
            if ($current + 1 < $total) {
257
                for ($i = $current; $i < $total - 1; $i++) {
258
                    $parameters[$i] = $ps[$i]->isDefaultValueAvailable() ? $ps[$i]->getDefaultValue() : null;
259
                }
260
            }
261
        } catch (ReflectionException $e) {
262
            //ignore
263
        }
264
        $parameters[] = $callback;
265
        call_user_func_array($callable, $parameters);
266
    }
267
268
    protected function _handleGenerator(Generator $flow, callable $callback, int $depth = 0)
269
    {
270
        $this->logGenerator($flow, $depth - 1);
271
        try {
272
            if (!$flow->valid()) {
273
                $callback(null, $flow->getReturn());
274
                if (!empty($flow->later)) {
275
                    $this->_awaitAll($flow->later, function ($error = null, $results = null) {
0 ignored issues
show
Bug introduced by
The property later does not seem to exist on Generator.
Loading history...
276
                    });
277
                    unset($flow->later);
278
                }
279
                return;
280
            }
281
            $value = $flow->current();
282
            $actions = $this->parse($flow->key() ?: Async::await);
283
            $next = function ($error = null, $result = null) use ($flow, $actions, $callback, $depth) {
284
                $value = $error ?: $result;
285
                if ($value instanceof Throwable) {
286
                    if (isset($actions['throw']) && is_a($value, $actions['throw'])) {
287
                        /** @scrutinizer ignore-call */
288
                        $flow->throw($value);
289
                        $this->_handleGenerator($flow, $callback, $depth);
290
                        return;
291
                    }
292
                    $callback($value, null);
293
                    return;
294
                }
295
                $flow->send($value);
296
                $this->_handleGenerator($flow, $callback, $depth);
297
            };
298
            if (key_exists(self::later, $actions)) {
299
                if (!isset($flow->later)) {
300
                    $flow->later = [];
301
                }
302
                if ($this->logger) {
303
                    $this->logger->info('later task scheduled', compact('depth'));
304
                }
305
                $flow->later[] = $value;
306
                return $next(null, $value);
307
            }
308
            if (key_exists(self::parallel, $actions)) {
309
                if (!isset($flow->parallel)) {
310
                    $flow->parallel = [];
0 ignored issues
show
Bug introduced by
The property parallel does not seem to exist on Generator.
Loading history...
311
                }
312
                $flow->parallel[] = $value;
313
                if (!isset($this->action)) {
314
                    $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...
315
                }
316
                $this->action[] = self::parallel;
317
                return $next(null, $value);
318
            }
319
            if (key_exists(self::all, $actions)) {
320
                $tasks = Async::parallel === $value && isset($flow->parallel) ? $flow->parallel : $value;
321
                unset($flow->parallel);
322
                if (is_array($tasks) && count($tasks)) {
323
                    if ($this->logger) {
324
                        $this->logger->info(
325
                            sprintf("all {%d} tasks awaited.", count($tasks)),
326
                            compact('depth')
327
                        );
328
                    }
329
                    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...
330
                }
331
                return $next(null, []);
332
            }
333
            $this->_handle($value, $next, $depth);
334
        } catch (Throwable $throwable) {
335
            $callback($throwable, null);
336
        }
337
    }
338
339
    /**
340
     * Handle known promise interfaces
341
     *
342
     * @param \React\Promise\PromiseInterface|\GuzzleHttp\Promise\PromiseInterface|\Amp\Promise|\Http\Promise\Promise $knownPromise
343
     * @param string $interface
344
     * @param callable $callback
345
     * @param int $depth
346
     * @return void
347
     */
348
    protected function _handlePromise($knownPromise, string $interface, callable $callback, int $depth = 0)
349
    {
350
        $this->logPromise($knownPromise, $interface, $depth);
351
        $resolver = function ($result) use ($callback) {
352
            $callback(null, $result);
353
        };
354
        $rejector = function ($error) use ($callback) {
355
            $callback($error, null);
356
        };
357
        try {
358
            switch ($interface) {
359
                case static::PROMISE_REACT:
360
                    $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

360
                    $knownPromise->/** @scrutinizer ignore-call */ 
361
                                   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...
361
                    break;
362
                case static::PROMISE_GUZZLE:
363
                    $knownPromise->then($resolver, $rejector);
364
                    if ($this->waitForGuzzleAndHttplug) {
365
                        if ($this->parallelGuzzleLoading) {
366
                            $this->guzzlePromises[] = $knownPromise;
367
                        } else {
368
                            $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

368
                            $knownPromise->/** @scrutinizer ignore-call */ 
369
                                           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

368
                            $knownPromise->/** @scrutinizer ignore-call */ 
369
                                           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...
369
                        }
370
                    }
371
                    break;
372
                case static::PROMISE_HTTP:
373
                    $knownPromise->then($resolver, $rejector);
374
                    if ($this->waitForGuzzleAndHttplug) {
375
                        $knownPromise->wait(false);
376
                    }
377
                    break;
378
                case static::PROMISE_AMP:
379
                    $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

379
                    $knownPromise->/** @scrutinizer ignore-call */ 
380
                                   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

379
                    $knownPromise->/** @scrutinizer ignore-call */ 
380
                                   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

379
                    $knownPromise->/** @scrutinizer ignore-call */ 
380
                                   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...
380
                        function ($error = null, $result = null) use ($resolver, $rejector) {
381
                            $error ? $rejector($error) : $resolver($result);
382
                        });
383
                    break;
384
            }
385
        } catch (\Exception $e) {
386
            $rejector($e);
387
        }
388
    }
389
390
    private function handleCommands(Generator $flow, &$value, callable $callback, int $depth): bool
391
    {
392
        $commands = $this->parse($flow->key());
393
        if ($value instanceof Throwable) {
394
            if (isset($commands['throw']) && is_a($value, $commands['throw'])) {
395
                $flow->throw($value);
0 ignored issues
show
Bug introduced by
The method throw() does not exist on Generator. ( Ignorable by Annotation )

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

395
                $flow->/** @scrutinizer ignore-call */ 
396
                       throw($value);

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...
396
                $this->_handleGenerator($flow, $callback, $depth);
397
                return true; //stop
398
            }
399
            $callback($value, null);
400
            return true; //stop
401
        }
402
        if (isset($commands[self::parallel])) {
403
            if (!isset($flow->parallel)) {
404
                $flow->parallel = [];
0 ignored issues
show
Bug introduced by
The property parallel does not seem to exist on Generator.
Loading history...
405
            }
406
            $flow->parallel [] = $value;
407
            return false; //continue
408
        }
409
410
        if (isset($commands[self::all])) {
411
            if (!isset($flow->parallel)) {
412
                $callback(null, []);
413
                return true; //stop
414
            }
415
            $this->_awaitAll(
416
                $flow->parallel,
417
                function ($error = null, $all = null) use ($flow, $callback, $depth) {
418
                    if ($error) {
419
                        $callback($error, false);
420
                        return;
421
                    }
422
                    $flow->send($all);
423
                    $this->_handleGenerator($flow, $callback, $depth);
424
                }
425
            );
426
            return true; //stop
427
        }
428
429
        return false; //continue
430
    }
431
432
    private function parse(string $command): array
433
    {
434
        $arr = [];
435
        if (strlen($command)) {
436
            parse_str(str_replace(['|', ':'], ['&', '='], $command), $arr);
437
        }
438
        return $arr;
439
    }
440
441
    private function action()
442
    {
443
        if (!empty($this->action)) {
444
            return array_shift($this->action);
445
        }
446
        return self::await;
447
    }
448
449
    private function logCallback(callable $callable, array $parameters, int $depth = 0)
450
    {
451
        if ($depth < 0 || !$this->logger) {
452
            return;
453
        }
454
        if (is_array($callable)) {
455
            $name = $callable[0];
456
            if (is_object($name)) {
457
                $name = '$' . lcfirst(get_class($name)) . '->' . $callable[1];
458
            } else {
459
                $name .= '::' . $callable[1];
460
            }
461
462
        } else {
463
            if (is_string($callable)) {
464
                $name = $callable;
465
            } elseif ($callable instanceof Closure) {
466
                $name = '$closure';
467
            } else {
468
                $name = '$callable';
469
            }
470
        }
471
        $this->logger->info(
472
            sprintf("%s %s%s", $this->action(), $name, $this->format($parameters)),
473
            compact('depth')
474
        );
475
    }
476
477
    private function logPromise(/** @scrutinizer ignore-unused */ $promise, string $interface, int $depth)
478
    {
479
        if ($depth < 0 || !$this->logger) {
480
            return;
481
        }
482
        $type = 'unknown';
483
        switch ($interface) {
484
            case static::PROMISE_REACT:
485
                $type = 'react';
486
                break;
487
            case static::PROMISE_GUZZLE:
488
                $type = 'guzzle';
489
                break;
490
            case static::PROMISE_HTTP:
491
                $type = 'httplug';
492
                break;
493
            case static::PROMISE_AMP:
494
                $type = 'amp';
495
                break;
496
        }
497
        $this->logger->info(
498
            sprintf("%s \$%sPromise;", $this->action(), $type),
499
            compact('depth')
500
        );
501
    }
502
503
    private function logGenerator(Generator $generator, int $depth = 0)
504
    {
505
        if ($depth < 0 || !$generator->valid() || !$this->logger) {
506
            return;
507
        }
508
        $info = new ReflectionGenerator($generator);
509
        $this->logReflectionFunction($info->getFunction(), $depth);
510
    }
511
512
    private function format($parameters)
513
    {
514
        return '(' . substr(json_encode($parameters), 1, -1) . ');';
515
    }
516
517
    private function logReflectionFunction(ReflectionFunctionAbstract $function, int $depth = 0)
518
    {
519
        if ($function instanceof ReflectionMethod) {
520
            $name = $function->getDeclaringClass()->getShortName();
521
            if ($function->isStatic()) {
522
                $name .= '::' . $function->name;
523
            } else {
524
                $name = '$' . lcfirst($name) . '->' . $function->name;
525
            }
526
        } elseif ($function->isClosure()) {
527
            $name = '$closure';
528
        } else {
529
            $name = $function->name;
530
        }
531
        $args = [];
532
        foreach ($function->getParameters() as $parameter) {
533
            $args[] = '$' . $parameter->name;
534
        }
535
        $this->logger->info(
536
            sprintf("%s %s(%s);", $this->action(), $name, implode(', ', $args)),
537
            compact('depth')
538
        );
539
    }
540
}
541