Passed
Push — master ( cf2834...aaf493 )
by devosc
02:59
created

Resolver.php$0 ➔ provision()   A

Complexity

Conditions 3

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 3
crap 3
1
<?php
2
/**
3
 *
4
 */
5
6
namespace Mvc5\Resolver;
7
8
use Mvc5\Arg;
9
use Mvc5\Plugin\Gem\Args;
10
use Mvc5\Plugin\Gem\Call;
11
use Mvc5\Plugin\Gem\Calls;
12
use Mvc5\Plugin\Gem\Child;
13
use Mvc5\Plugin\Gem\Config;
14
use Mvc5\Plugin\Gem\Copy;
15
use Mvc5\Plugin\Gem\Expect;
16
use Mvc5\Plugin\Gem\Factory;
17
use Mvc5\Plugin\Gem\FileInclude;
18
use Mvc5\Plugin\Gem\Filter;
19
use Mvc5\Plugin\Gem\Gem;
20
use Mvc5\Plugin\Gem\Invokable;
21
use Mvc5\Plugin\Gem\Invoke;
22
use Mvc5\Plugin\Gem\Link;
23
use Mvc5\Plugin\Gem\Param;
24
use Mvc5\Plugin\Gem\Plug;
25
use Mvc5\Plugin\Gem\Plugin;
26
use Mvc5\Plugin\Gem\Provide;
27
use Mvc5\Plugin\Gem\Scoped;
28
use Mvc5\Plugin\Gem\Shared;
29
use Mvc5\Plugin\Gem\SignalArgs;
30
use Mvc5\Plugin\Gem\Value;
31
use Mvc5\Resolvable;
32
33
use function array_merge;
34
use function explode;
35
use function is_array;
36
use function is_string;
37
use function is_object;
38
use function key;
39
use function serialize;
40
use function unserialize;
41
42
trait Resolver
43
{
44
    /**
45
     *
46
     */
47
    use Build;
48
    use Container;
49
    use Generator;
50
    use Service;
51
52
    /**
53
     * @var callable
54
     */
55
    protected $provider;
56
57
    /**
58
     * @var object
59
     */
60
    protected $scope;
61
62
    /**
63
     * @param array|\ArrayAccess|null $config
64
     * @param callable|null $provider
65
     * @param bool|object|null $scope
66
     * @param bool $strict
67
     * @throws \Throwable
68
     */
69 301
    function __construct($config = null, callable $provider = null, $scope = null, bool $strict = false)
70
    {
71 301
        $config && $this->config = $config;
72
73 301
        isset($config[Arg::CONTAINER])
74 29
            && $this->container = $config[Arg::CONTAINER];
75
76 301
        isset($config[Arg::EVENTS])
77 25
            && $this->events = $config[Arg::EVENTS];
78
79 301
        isset($config[Arg::SERVICES])
80 109
            && $this->services = $config[Arg::SERVICES];
81
82 301
        $provider && $this->provider = $this->resolve($provider);
83
84 301
        $scope && $this->scope = $this->resolve($scope);
85
86 301
        $strict && $this->strict = $strict;
87 301
    }
88
89
    /**
90
     * @param array|mixed $args
91
     * @return array|mixed
92
     * @throws \Throwable
93
     */
94 94
    protected function args($args)
95
    {
96 94
        if (!$args) {
97 56
            return $args;
98
        }
99
100 68
        if (!is_array($args)) {
101 1
            return $this->resolve($args);
102
        }
103
104 67
        foreach($args as $index => $value) {
105 67
            $value instanceof Resolvable && $args[$index] = $this->resolve($value);
106
        }
107
108 67
        return $args;
109
    }
110
111
    /**
112
     * @param array $child
113
     * @param array $parent
114
     * @return array
115
     */
116 38
    protected function arguments(array $child, array $parent) : array
117
    {
118 38
        return !$parent ? $child : (
119 38
            !$child ? $parent : (is_string(key($child)) ? $child + $parent : array_merge($child, $parent))
120
        );
121
    }
122
123
    /**
124
     * @param \Closure $callback
125
     * @param object $object
126
     * @param bool $scoped
127
     * @return \Closure
128
     */
129 7
    protected function bind(\Closure $callback, $object, $scoped) : \Closure
130
    {
131 7
        return \Closure::bind($callback, $object, $scoped ? $object : null);
132
    }
133
134
    /**
135
     * @param Child $child
136
     * @param array $args
137
     * @return mixed
138
     * @throws \Throwable
139
     */
140 3
    protected function child(Child $child, array $args = [])
141
    {
142 3
        return $this->provide($this->merge($this->parent($child->parent()), $child), $args);
143
    }
144
145
    /**
146
     * @param mixed $value
147
     * @param iterable $filters
148
     * @param array $args
149
     * @param string|null $param
150
     * @return mixed
151
     * @throws \Throwable
152
     */
153 5
    protected function filter($value, iterable $filters = [], array $args = [], string $param = null)
154
    {
155 5
        $result = $value;
156
157 5
        foreach($filters as $filter) {
158 5
            $value = $this->invoke(
159 5
                $this->callable($filter), $param ? [$param => $result] + $args : array_merge([$result], $args)
160
            );
161
162 5
            if (false === $value) {
163 1
                return $result;
164
            }
165
166 5
            if (null === $value) {
167 1
                return null;
168
            }
169
170 5
            $result = $value;
171
        }
172
173 3
        return $result;
174
    }
175
176
    /**
177
     * @param Filter $filter
178
     * @param array $args
179
     * @return mixed
180
     * @throws \Throwable
181
     */
182 5
    protected function filterable(Filter $filter, array $args = [])
183
    {
184 5
        return $this->filter(
185 5
            $this->resolve($filter->config()), $this->resolve($filter->filter()), $args, $filter->param()
0 ignored issues
show
Bug introduced by
It seems like $this->resolve($filter->filter()) can also be of type string; however, parameter $filters of Mvc5\Resolver\Resolver::filter() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

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

185
            $this->resolve($filter->config()), /** @scrutinizer ignore-type */ $this->resolve($filter->filter()), $args, $filter->param()
Loading history...
186
        );
187
    }
188
189
    /**
190
     * @param Gem $gem
191
     * @param array $args
192
     * @return callable|mixed
193
     * @throws \Throwable
194
     */
195 113
    protected function gem(Gem $gem, array $args = [])
196
    {
197 113
        if ($gem instanceof Factory) {
198 1
            return $this->invoke($this->child($gem, $args));
0 ignored issues
show
Bug introduced by
$this->child($gem, $args) of type object is incompatible with the type callable expected by parameter $callable of Mvc5\Resolver\Resolver::invoke(). ( Ignorable by Annotation )

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

198
            return $this->invoke(/** @scrutinizer ignore-type */ $this->child($gem, $args));
Loading history...
199
        }
200
201 112
        if ($gem instanceof Calls) {
202 1
            return $this->hydrate($gem, $this->resolve($gem->name(), $args));
203
        }
204
205 112
        if ($gem instanceof Child) {
206 2
            return $this->child($gem, $args);
207
        }
208
209 110
        if ($gem instanceof Plugin) {
210 45
            return $this->provide($gem, $args);
211
        }
212
213 87
        if ($gem instanceof Shared) {
214 8
            return $this->shared($gem->name(), $gem->config());
215
        }
216
217 85
        if ($gem instanceof Param) {
218 20
            return $this->resolve($this->param($gem->name()), $args);
219
        }
220
221 84
        if ($gem instanceof Call) {
222 23
            return $this->call($this->resolve($gem->config()), $this->vars($args, $gem->args()));
223
        }
224
225 72
        if ($gem instanceof Args) {
226 9
            return $this->args($gem->config());
227
        }
228
229 68
        if ($gem instanceof Config) {
230 3
            return $this->config();
231
        }
232
233 65
        if ($gem instanceof Link) {
234 20
            return $this;
235
        }
236
237 47
        if ($gem instanceof Filter) {
238 5
            return $this->filterable($gem, $this->vars($args, $gem->args()));
239
        }
240
241 42
        if ($gem instanceof Plug) {
242 4
            return $this->configured($gem->name());
243
        }
244
245 39
        if ($gem instanceof Invoke) {
246
            return function(...$argv) use ($gem) {
247 9
                return $this->resolve($this->call(
248 9
                    $this->resolve($gem->config()), $this->vars($this->variadic($argv), $gem->args())
249
                ));
250 9
            };
251
        }
252
253 35
        if ($gem instanceof Invokable) {
254
            return function(...$argv) use ($gem) {
255 4
                return $this->resolve($gem->config(), $this->vars($this->variadic($argv), $gem->args()));
256 4
            };
257
        }
258
259 31
        if ($gem instanceof FileInclude) {
260
            return (new class() {
261
                function __invoke($file) {
1 ignored issue
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
262 1
                    return include $file;
263
                }
264
            })($this->resolve($gem->config()));
265
        }
266
267 30
        if ($gem instanceof Copy) {
268 1
            return clone $this->resolve($gem->config(), $args);
269
        }
270
271 29
        if ($gem instanceof Value) {
272 16
            return $gem->config();
273
        }
274
275 14
        if ($gem instanceof Scoped) {
276 5
            return $this->scoped($gem->closure(), $gem->scoped());
277
        }
278
279 9
        if ($gem instanceof Provide) {
280 3
            return ($this->provider() ?? new Unresolvable)($gem->config(), $this->vars($args, $gem->args()));
281
        }
282
283 6
        if ($gem instanceof Expect) {
284
            try {
285 5
                return $this->resolve($gem->plugin(), $args);
286 3
            } catch(\Throwable $exception) {
287 3
                return $this->resolve($gem->exception(), $gem->args($exception, $args));
288
            }
289
        }
290
291 1
        return Unresolvable::plugin($gem);
292
    }
293
294
    /**
295
     * @param Plugin $plugin
296
     * @param object $service
297
     * @return object
298
     * @throws \Throwable
299
     */
300 48
    protected function hydrate(Plugin $plugin, $service)
301
    {
302 48
        foreach($plugin->calls() as $method => $args) {
303 14
            if (is_string($method)) {
304 5
                if (Arg::INDEX == $method[0]) {
305 2
                    $service[substr($method, 1)] = $this->resolve($args);
306 2
                    continue;
307
                }
308
309 3
                if (Arg::PROPERTY == $method[0]) {
310 2
                    $service->{substr($method, 1)} = $this->resolve($args);
311 2
                    continue;
312
                }
313
314 1
                $service->$method($this->resolve($args));
315 1
                continue;
316
            }
317
318 9
            if (is_array($args)) {
319 8
                $method = array_shift($args);
320 8
                $param  = $plugin->param();
321
322 8
                if (is_string($method) && Arg::PROPERTY == $method[0]) {
323 3
                    $param  = substr($method, 1);
324 3
                    $method = array_shift($args);
325
                }
326
327 8
                $this->invoke(
328 8
                    is_string($method) ? [$service, $method] : $this->callable($method),
329 8
                    ($param && (!$args || is_string(key($args))) ? [$param => $service] : []) + $this->args($args)
330
                );
331
332 8
                continue;
333
            }
334
335 1
            $this->resolve($args);
336
        }
337
338 48
        return $service;
339
    }
340
341
    /**
342
     * @param Plugin $parent
343
     * @param Plugin $child
344
     * @param string|null $name
345
     * @param array $config
346
     * @return Plugin
347
     * @throws \Throwable
348
     */
349 8
    protected function merge(Plugin $parent, Plugin $child, string $name = null, array $config = []) : Plugin
350
    {
351 8
        !$parent->name() &&
352 1
            $config[Arg::NAME] = $name ?? $this->resolve($child->name());
353
354 8
        $child->args() &&
355 5
            $config[Arg::ARGS] = is_string(key($child->args())) ? $child->args() + $parent->args() : $child->args();
356
357 8
        $child->calls() &&
358 2
            $config[Arg::CALLS] = $child->merge() ? array_merge($parent->calls(), $child->calls()) : $child->calls();
359
360 8
        $child->param() &&
361 5
            $config[Arg::PARAM] = $child->param();
362
363 8
        return $config ? $parent->with($config) : $parent;
364
    }
365
366
    /**
367
     * @param array|string $name
368
     * @return mixed
369
     */
370 24
    function param($name)
371
    {
372 24
        if (is_string($name)) {
373 24
            return param($this->config(), $name);
374
        }
375
376 4
        $matched = [];
377
378 4
        foreach($name as $key) {
379 4
            $matched[$key] = $this->config()[$key] ?? null;
380
        }
381
382 4
        return $matched;
383
    }
384
385
    /**
386
     * @param string $parent
387
     * @return Plugin
388
     * @throws \Throwable
389
     */
390 3
    protected function parent(string $parent) : Plugin
391
    {
392 3
        return $this->configured($this->resolve($parent));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->configured($this->resolve($parent)) could return the type null which is incompatible with the type-hinted return Mvc5\Plugin\Gem\Plugin. Consider adding an additional type-check to rule them out.
Loading history...
393
    }
394
395
    /**
396
     * @param string|mixed $plugin
397
     * @param array $args
398
     * @param callable|null $callback
399
     * @param string|null $previous
400
     * @return mixed
401
     * @throws \Throwable
402
     */
403 199
    function plugin($plugin, array $args = [], callable $callback = null, string $previous = null)
404
    {
405 199
        if (!$plugin) {
406 2
            return $plugin;
407
        }
408
409 198
        if (is_string($plugin)) {
410 124
            return $this->build(explode(Arg::SERVICE_SEPARATOR, $plugin), $args, $callback);
411
        }
412
413 150
        if (is_array($plugin)) {
414 27
            return $this->pluginArray(array_shift($plugin), $args + $this->args($plugin), $callback, $previous);
415
        }
416
417 124
        if ($plugin instanceof \Closure) {
418 20
            return $this->invoke($this->scoped($plugin), $args);
419
        }
420
421 111
        return $this->resolve($plugin, $args);
422
    }
423
424
    /**
425
     * @param string|mixed $plugin
426
     * @param array $args
427
     * @param callable|null $callback
428
     * @param string|null $previous
429
     * @return mixed
430
     * @throws \Throwable
431
     */
432 27
    protected function pluginArray($plugin, array $args = [], callable $callback = null, string $previous = null)
433
    {
434 27
        return $previous && $previous === $plugin ?
435 27
            $this->callback($plugin, true, $args, $callback) : $this->plugin($plugin, $args, $callback);
436
    }
437
438
    /**
439
     * @param Plugin $plugin
440
     * @param array $args
441
     * @return callable|object|null
442
     * @throws \Throwable
443
     */
444 48
    protected function provide(Plugin $plugin, array $args = [])
445
    {
446 48
        $name   = $this->resolve($plugin->name());
447 48
        $parent = $this->configured($name);
448
449 48
        $args && is_string(key($args)) && $plugin->args() && $args += $this->args($plugin->args());
0 ignored issues
show
Bug Best Practice introduced by
The expression $args 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...
450
451 48
        !$args && $args = $this->args($plugin->args());
452
453 48
        if (!$parent) {
454 30
            return $this->hydrate($plugin, $this->combine(explode(Arg::SERVICE_SEPARATOR, $name), $args));
0 ignored issues
show
Bug introduced by
It seems like $this->combine(explode(M...PARATOR, $name), $args) can also be of type mixed; however, parameter $service of Mvc5\Resolver\Resolver::hydrate() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

454
            return $this->hydrate($plugin, /** @scrutinizer ignore-type */ $this->combine(explode(Arg::SERVICE_SEPARATOR, $name), $args));
Loading history...
455
        }
456
457 27
        if (!$parent instanceof Plugin) {
458 21
            return $this->hydrate(
459 21
                $plugin, $name === $parent ? $this->make($name, $args) : $this->provision($this->resolve($parent), $args)
460
            );
461
        }
462
463 6
        if ($name === $parent->name()) {
464 1
            return $this->hydrate($plugin, $this->make($name, $args));
465
        }
466
467 5
        return $this->provide($this->merge($parent, $plugin, $name), $args);
468
    }
469
470
    /**
471
     * @return callable|null
472
     */
473 63
    protected function provider() : ?callable
474
    {
475 63
        return $this->provider;
476
    }
477
478
    /**
479
     * @param $plugin
480
     * @param array $args
481
     * @return mixed
482
     * @throws \ReflectionException
483
     * @throws \Throwable
484
     */
485
    protected function provision($plugin, array $args)
486 154
    {
487
        return $plugin instanceof \Closure && (new \ReflectionFunction($plugin))->getClosureThis() ?
488 154
            $this->invoke($plugin, $args) : $this->plugin($plugin, $args);
489 114
    }
490 154
491
    /**
492
     * @param Resolvable|mixed $plugin
493
     * @param array $args
494
     * @param callable|null $callback
495
     * @param int $c
496
     * @return mixed
497
     * @throws \Throwable
498
     */
499
    protected function resolvable($plugin, array $args = [], callable $callback = null, int $c = 0)
500 154
    {
501
        return !$plugin instanceof Resolvable ? $plugin : (
502 154
            $c > Arg::MAX_RECURSION ? Unresolvable::plugin($plugin) :
503
                $this->resolvable($this->solve($plugin, $args, $callback), $args, $callback, ++$c)
504
        );
505
    }
506
507
    /**
508
     * @param Resolvable|mixed $plugin
509
     * @param array $args
510
     * @return mixed
511 1
     * @throws \Throwable
512
     */
513 1
    protected function resolve($plugin, array $args = [])
514
    {
515
        return $this->resolvable($plugin, $args);
516
    }
517
518
    /**
519
     * @param string|mixed $plugin
520 10
     * @param array $args
521
     * @return mixed
522 10
     * @throws \Throwable
523
     */
524
    protected function resolver($plugin, array $args = [])
525
    {
526
        return $this->call($this->provider() ?? Arg::SERVICE_RESOLVER, [$plugin, $args]);
527
    }
528
529
    /**
530 25
     * @param object $scope
531
     * @return bool|object|null
532 25
     */
533
    function scope($scope = null)
534
    {
535
        return null !== $scope ? $this->scope = $scope : $this->scope;
536
    }
537
538
    /**
539
     * @param \Closure $callback
540
     * @param bool $scoped
541
     * @return \Closure
542 114
     */
543
    protected function scoped(\Closure $callback, bool $scoped = false) : \Closure
544 114
    {
545 112
        return $this->scope ? $this->bind($callback, $this->scope === true ? $this : $this->scope, $scoped) : $callback;
0 ignored issues
show
introduced by
The condition $this->scope === true is always false. If $this->scope === true can have other possible types, add them to src/Resolver/Resolver.php:58
Loading history...
546
    }
547
548
    /**
549
     * @param Gem|mixed $plugin
550
     * @param array $args
551
     * @param callable|null $callback
552 4
     * @return callable|mixed
553
     * @throws \Throwable
554 4
     */
555
    protected function solve($plugin, array $args = [], callable $callback = null)
556
    {
557
        return $plugin instanceof Gem ? $this->gem($plugin, $args) : (
558
            $callback ? $callback($plugin, $args) : $this->resolver($plugin, $args)
559
        );
560 4
    }
561
562
    /**
563 4
     * @return string
564 4
     */
565 4
    function serialize() : string
566
    {
567
        return serialize([$this->config, $this->events, $this->provider, $this->scope, $this->services, $this->strict]);
568
    }
569
570
    /**
571 25
     * @param string $serialized
572
     */
573 25
    function unserialize($serialized) : void
574
    {
575
        list(
576
            $this->config, $this->events, $this->provider, $this->scope, $this->services, $this->strict
577
        ) = unserialize($serialized);
578
    }
579
580
    /**
581
     * @param array $args
582 38
     * @return array
583
     */
584 38
    protected function variadic(array $args) : array
585
    {
586
        return $args && $args[0] instanceof SignalArgs ? $args[0]->args() : $args;
0 ignored issues
show
Bug Best Practice introduced by
The expression $args 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...
587
    }
588
589
    /**
590
     * @param array $child
591
     * @param array $parent
592
     * @return array
593 1
     * @throws \Throwable
594
     */
595 1
    protected function vars(array $child = [], array $parent = []) : array
596
    {
597
        return $this->arguments($child, $this->args($parent));
598
    }
599
600
    /**
601 14
     * @param mixed $plugin
602
     * @param array $args
603 14
     * @return mixed
604 3
     * @throws \Throwable
605
     */
606 14
    function __call($plugin, array $args = [])
607 3
    {
608
        return $this->call($plugin, $args);
609 3
    }
610 3
611
    /**
612
     *
613
     */
614 14
    function __clone()
615 2
    {
616
        is_object($this->config) &&
617 2
            $this->config = clone $this->config;
618 2
619
        if (is_object($this->container)) {
620
            $this->container = clone $this->container;
621
622 14
            if (isset($this->config[Arg::CONTAINER])) {
623 3
                $this->config[Arg::CONTAINER] = $this->container;
624
            }
625 3
        }
626 3
627
        if (is_object($this->events)) {
628
            $this->events = clone $this->events;
629
630 14
            if (isset($this->config[Arg::EVENTS])) {
631 2
                $this->config[Arg::EVENTS] = $this->events;
632 14
            }
633
        }
634
635
        if (is_object($this->services)) {
636
            $this->services = clone $this->services;
637
638
            if (isset($this->config[Arg::SERVICES])) {
639
                $this->config[Arg::SERVICES] = $this->services;
640
            }
641
        }
642
643
        is_object($this->scope) &&
644
            $this->scope = clone $this->scope;
645
    }
646
647
    /**
648
     * @param mixed $plugin
649
     * @param array $args
650
     * @return mixed
651
     * @throws \Throwable
652
     */
653 24
    function __invoke($plugin, array $args = [])
654 24
    {
655
        return $this->plugin($plugin, $args, $this->provider() ?? function(){});
656 24
    }
657 22
}
658
659
/**
660 24
 * @param array|\ArrayAccess $config
661
 * @param string $name
662
 * @return mixed
663
 */
664
function param($config, string $name)
665
{
666
    $name = explode(Arg::CALL_SEPARATOR, $name);
667
    $value = $config[array_shift($name)];
668
669
    foreach($name as $n) {
670
        $value = $value[$n];
671
    }
672
673
    return $value;
674
}
675