Completed
Pull Request — 1.0 (#2)
by David
04:25
created

ServiceProviderLoader::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\Yaco\ServiceProvider;
5
6
7
use Interop\Container\Definition\DefinitionInterface;
8
use Puli\Discovery\Api\Discovery;
9
use Puli\Discovery\Binding\ClassBinding;
10
use TheCodingMachine\Yaco\Compiler;
11
use TheCodingMachine\Yaco\Definition\AliasDefinition;
12
use TheCodingMachine\Yaco\Definition\DumpableInterface;
13
use TheCodingMachine\Yaco\Definition\FactoryCallDefinition;
14
use TheCodingMachine\Yaco\Definition\Reference;
15
use TheCodingMachine\Yaco\DefinitionConverter;
16
use TheCodingMachine\Yaco\DefinitionConverterInterface;
17
18
class ServiceProviderLoader
19
{
20
    /**
21
     * @var Compiler
22
     */
23
    private $compiler;
24
25
    /**
26
     * @var DefinitionConverterInterface
27
     */
28
    private $converter;
29
30
    /**
31
     * @param Compiler $compiler
32
     */
33
    public function __construct(Compiler $compiler, DefinitionConverterInterface $converter = null)
34
    {
35
        $this->compiler = $compiler;
36
        if ($converter === null) {
37
            $converter = new DefinitionConverter();
38
        }
39
        $this->converter = $converter;
40
    }
41
42
    /**
43
     * Discovers service provider class names using Puli.
44
     *
45
     * @param Discovery $discovery
46
     * @return string[] Returns an array of service providers.
47
     */
48
    public function discover(Discovery $discovery) : array {
49
        $bindings = $discovery->findBindings('container-interop/service-provider');
50
        $serviceProviders = [];
51
52
        foreach ($bindings as $binding) {
53
            if ($binding instanceof ClassBinding) {
54
                $serviceProviders[] = $binding->getClassName();
55
            }
56
        }
57
        return $serviceProviders;
58
    }
59
60
    /**
61
     * Discovers and loads the service providers using Puli.
62
     *
63
     * @param Discovery $discovery
64
     */
65
    public function discoverAndLoad(Discovery $discovery) {
66
        $serviceProviders = $this->discover($discovery);
67
68
        foreach ($serviceProviders as $serviceProvider) {
69
            $this->load($serviceProvider);
70
        }
71
    }
72
73
    public function load(string $serviceProviderClassName)
74
    {
75
        if (!class_exists($serviceProviderClassName)) {
76
            throw new InvalidArgumentException(sprintf('ServiceProviderLoader::load expects a valid class name. Could not find class "%s"', $serviceProviderClassName));
77
        }
78
79
        $serviceFactories = call_user_func([$serviceProviderClassName, 'getServices']);
80
81
        foreach ($serviceFactories as $serviceName => $callable) {
82
            $this->registerService($serviceName, $serviceProviderClassName, $callable);
83
        }
84
    }
85
86
    private function registerService(string $serviceName, string $className, callable $callable) {
87
        if (!$this->compiler->has($serviceName)) {
88
            $definition = $this->getServiceDefinitionFromCallable($serviceName, $className, $callable, [new ContainerDefinition()]);
89
90
            $this->compiler->addDumpableDefinition($definition);
91
        } else {
92
            // The new service will be created under the name 'xxx_decorated_y'
93
            // The old service will be moved to the name 'xxx_decorated_y.inner'
94
            // This old service will be accessible through a callback represented by 'xxx_decorated_y.callbackwrapper'
95
            // The $servicename becomes an alias pointing to 'xxx_decorated_y'
96
97
            $previousDefinition = $this->compiler->getDumpableDefinition($serviceName);
98
            while ($previousDefinition instanceof Reference) {
99
                $previousDefinition = $this->compiler->getDumpableDefinition($previousDefinition->getAlias());
100
            }
101
102
            $oldServiceName = $serviceName;
103
            $serviceName = $this->getDecoratedServiceName($serviceName);
104
            $innerName = $serviceName.'.inner';
105
            $callbackWrapperName = $serviceName.'.callbackwrapper';
106
107
            $innerDefinition = new FactoryCallDefinition($innerName, $previousDefinition->getFactory(), $previousDefinition->getMethodName(), $previousDefinition->getMethodArguments());
108
109
110
            $callbackWrapperDefinition = new CallbackWrapperDefinition($callbackWrapperName, $innerDefinition);
111
112
            $definition = $this->getServiceDefinitionFromCallable($serviceName, $className, $callable, [new ContainerDefinition(), $callbackWrapperDefinition]);
113
114
            $this->compiler->addDumpableDefinition($definition);
115
            $this->compiler->addDumpableDefinition($innerDefinition);
116
            $this->compiler->addDumpableDefinition($callbackWrapperDefinition);
117
            $this->compiler->addDumpableDefinition(new AliasDefinition($oldServiceName, $serviceName));
118
        }
119
120
    }
121
122
    private function getServiceDefinitionFromCallable(string $serviceName, string $className, callable $callable, array $params) : DumpableInterface {
123
        if ($callable instanceof DefinitionInterface) {
124
            return $this->converter->convert($serviceName, $callable);
125
        }
126
        if (is_array($callable) && is_string($callable[0])) {
127
            return new FactoryCallDefinition($serviceName, $callable[0], $callable[1], $params);
0 ignored issues
show
Documentation introduced by
$callable[0] is of type string, but the function expects a object<TheCodingMachine\...ion\ReferenceInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
128
        }
129
        // This is an object or a callback... we need to call the getServices method of the service provider at runtime.
130
        $factoryParams = [
131
            $className,
132
            $serviceName,
133
            $params
134
        ];
135
        return new FactoryCallDefinition($serviceName, ServiceFactory::class, 'create', $factoryParams);
136
    }
137
138
    private function getDecoratedServiceName(string $serviceName) : string {
139
        $counter = 1;
140
        while ($this->compiler->has($serviceName.'_decorated_'.$counter)) {
141
            $counter++;
142
        }
143
        return $serviceName.'_decorated_'.$counter;
144
    }
145
}