Passed
Push — master ( 955b97...98d698 )
by Dan
01:03
created

validateArgumentProperties()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 2
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace danmurf\DependencyInjection;
4
5
use danmurf\DependencyInjection\Exception\CircularReferenceException;
6
use danmurf\DependencyInjection\Exception\ContainerException;
7
use danmurf\DependencyInjection\Exception\NotFoundException;
8
use Psr\Container\ContainerInterface;
9
use ReflectionClass;
10
11
/**
12
 * Service locator which can get find and build services using
13
 * pre-defined configuration.
14
 *
15
 * @author Dan Murfitt <[email protected]>
16
 */
17
class ConfigurableServiceLocator implements ServiceLocatorInterface
18
{
19
    const ARGUMENT_TYPE_SCALAR = 'scalar';
20
    const ARGUMENT_TYPE_SERVICE = 'service';
21
22
    const ARGUMENT_TYPES = [
23
            self::ARGUMENT_TYPE_SCALAR,
24
            self::ARGUMENT_TYPE_SERVICE,
25
        ];
26
27
    /** @var array */
28
    private $config = [];
29
30
    /** @var array */
31
    private $locateCalls = [];
32
33
    /**
34
     * @param array $config
35
     */
36 21
    public function __construct(array $config)
37
    {
38 21
        $this->validateConfig($config);
39 17
        $this->config = $config;
40 17
    }
41
42
    /**
43
     * Get a new instance of a service.
44
     *
45
     * @param string             $id        The service id
46
     * @param ContainerInterface $container The container to get service dependencies from
47
     *
48
     * @throws ContainerException
49
     * @throws NotFoundException
50
     */
51 10
    public function locate($id, ContainerInterface $container)
52
    {
53 10
        $this->validateRequest($id);
54 10
        $this->handleServiceNotFound($id);
55
56 5
        if (isset($this->config[$id]['service'])) {
57 1
            return $this->locate($this->config[$id]['service'], $container);
58
        }
59
60 5
        $class = new ReflectionClass($this->config[$id]['class']);
61
62 5
        if (!isset($this->config[$id]['arguments'])) {
63 2
            return $class->newInstance();
64
        }
65
66 3
        return $class->newInstanceArgs(
67 3
            $this->getArgs($id, $container)
68
        );
69
    }
70
71
    /**
72
     * @param string $id
73
     */
74 10
    private function handleServiceNotFound(string $id)
75
    {
76 10
        if (!isset($this->config[$id])) {
77 5
            throw new NotFoundException(sprintf('Unable to locate service `%s` from configuration.', $id));
78
        }
79 5
    }
80
81
    /**
82
     * Get the argument values/instances for a specific service.
83
     *
84
     * @param string             $id
85
     * @param ContainerInterface $container
86
     *
87
     * @return array
88
     */
89 3
    private function getArgs(string $id, ContainerInterface $container): array
90
    {
91 3
        $args = [];
92 3
        foreach ($this->config[$id]['arguments'] as $argumentConfig) {
93 3
            switch ($argumentConfig['type']) {
94 3
                case self::ARGUMENT_TYPE_SCALAR:
95 1
                    $args[] = $argumentConfig['value'];
96 1
                    break;
97
98 2
                case self::ARGUMENT_TYPE_SERVICE:
99 2
                    $args[] = $container->get($argumentConfig['value']);
100 2
                    break;
101
            }
102
        }
103
104 2
        return $args;
105
    }
106
107
    /**
108
     * Ensure the config is in a valid format.
109
     *
110
     * @param array $config
111
     *
112
     * @throws ContainerException
113
     */
114 21
    private function validateConfig(array $config)
115
    {
116 21
        foreach ($config as $id => $definition) {
117 10
            if (isset($definition['class'])) {
118
                // Class service definition
119 6
                $this->validateClassDefinition($definition, $id);
120
121 4
                return;
122
            }
123
124 4
            if (isset($definition['service'])) {
125
                // Interface service mapping
126 2
                return;
127
            }
128
129 2
            throw new ContainerException(sprintf('Configured service `%s` has no `class` or `interface` value.', $id));
130
        }
131 11
    }
132
133
    /**
134
     * Ensure a class definied service has valid config.
135
     *
136
     * @param array  $definition
137
     * @param string $id
138
     */
139 6
    private function validateClassDefinition(array $definition, string $id)
140
    {
141 6
        if (isset($definition['arguments'])) {
142 4
            $this->validateArguments($definition['arguments'], $id);
143
        }
144 4
    }
145
146
    /**
147
     * Ensure the arguments config is in a valid format.
148
     *
149
     * @param array  $arguments
150
     * @param string $id
151
     *
152
     * @throws ContainerException
153
     */
154 4
    private function validateArguments(array $arguments, string $id)
155
    {
156 4
        foreach ($arguments as $argument) {
157 4
            $this->validateArgumentProperties($argument, $id);
158 3
            $this->validateArgumentType($argument['type'], $id);
159
        }
160 2
    }
161
162
    /**
163
     * Ensure the argument has valid properties.
164
     *
165
     * @param array  $argument
166
     * @param string $id
167
     *
168
     * @throws ContainerException
169
     */
170 4
    private function validateArgumentProperties(array $argument, string $id)
171
    {
172 4
        if (!isset($argument['type']) || !isset($argument['value'])) {
173 1
            throw new ContainerException(sprintf(
174 1
                'Configuration for service `%s` must have `type` and `value` values.',
175 1
                $id
176
            ));
177
        }
178 3
    }
179
180
    /**
181
     * Ensure the argument type is valid.
182
     *
183
     * @param string $type
184
     * @param string $id
185
     *
186
     * @throws ContainerException
187
     */
188 3
    private function validateArgumentType(string $type, string $id)
189
    {
190 3
        if (false === array_search($type, self::ARGUMENT_TYPES)) {
191 1
            throw new ContainerException(sprintf(
192 1
                'Unknown argument type `%s` for service `%s`. Accepted values are `scalar` and `service`.',
193 1
                $type,
194 1
                $id
195
            ));
196
        }
197 2
    }
198
199
    /**
200
     * @param string $id
201
     *
202
     * @throws CircularReferenceException
203
     */
204 10
    private function validateRequest(string $id)
205
    {
206 10
        if (false !== array_search($id, $this->locateCalls)) {
207 1
            throw new CircularReferenceException(sprintf('Circular dependency reference detected for `%s`', $id));
208
        }
209
210 10
        $this->locateCalls[] = $id;
211 10
    }
212
}
213