Test Failed
Push — master ( f0d439...d307a0 )
by mcfog
01:31
created

Factory::produceFromClass()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 17
ccs 10
cts 10
cp 1
rs 8.8333
c 0
b 0
f 0
cc 7
nc 3
nop 3
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Lit\Air;
6
7
use Lit\Air\Injection\InjectorInterface;
8
use Lit\Air\Psr\CircularDependencyException;
9
use Lit\Air\Psr\Container;
10
use Lit\Air\Psr\ContainerException;
11
use Psr\Container\ContainerInterface;
12
13
class Factory implements ContainerInterface
14
{
15
    const CONTAINER_KEY = self::class;
16
    const KEY_INJECTORS = self::class . '::injectors';
17
    /**
18
     * @var Container
19
     */
20
    protected $container;
21
    /**
22
     * @var array
23
     */
24
    protected $circularStore = [];
25
26 7
    public function __construct(ContainerInterface $container = null)
27
    {
28 7
        if ($container instanceof Container) {
29 7
            $this->container = $container;
30
        } elseif (is_null($container)) {
31
            $this->container = new Container();
32
        } else {
33
            $this->container = Container::wrap($container);
34
        }
35 7
        $this->container->set(self::CONTAINER_KEY, $this);
36 21
    }
37
38 21
    public static function of(ContainerInterface $container): self
39 1
    {
40 7
        if (!$container->has(self::CONTAINER_KEY)) {
41 7
            return new self($container);
42 14
        }
43
44 4
        return $container->get(self::CONTAINER_KEY);
45
    }
46
47
    /**
48 1
     * @param string $className
49
     * @param array $extraParameters
50 1
     * @return object
51 1
     * @throws \Psr\Container\ContainerExceptionInterface
52
     * @throws \ReflectionException
53
     */
54 5
    public function instantiate(string $className, array $extraParameters = [])
55
    {
56 5
        $class = new \ReflectionClass($className);
57 5
        $constructor = $class->getConstructor();
58
59 5
        $constructParams = $constructor
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
60 3
            ? $this->resolveParams($constructor->getParameters(), $className, $extraParameters)
61 5
            : [];
62
63 5
        $instance = $class->newInstanceArgs($constructParams);
64 4
        $this->inject($instance, $extraParameters);
65
66 4
        return $instance;
67
    }
68
69 1
    /**
70 1
     * @param callable $callback
71 1
     * @param array $extra
72
     * @return mixed
73
     * @throws \ReflectionException
74
     */
75 4
    public function invoke(callable $callback, array $extra = [])
76
    {
77 4
        if (is_string($callback) || $callback instanceof \Closure) {
78 4
            $method = new \ReflectionFunction($callback);
79 4
            $params = $method->getParameters();
80
        } else {
81 2
            if (is_object($callback)) {
82 1
                $callback = [$callback, '__invoke'];
83
            }
84 2
            $method = (new \ReflectionClass($callback[0]))->getMethod($callback[1]);
85 2
            $params = $method->getParameters();
86
        }
87
88 4
        if ($method->isClosure()) {
89 4
            $name = sprintf('Closure@%s:%d', $method->getFileName(), $method->getStartLine());
90 2
        } elseif ($method instanceof \ReflectionMethod) {
91 2
            $name = sprintf('%s::%s', $method->getDeclaringClass()->name, $method->name);
92
        } else {
93 1
            assert($method instanceof \ReflectionFunction);
94 1
            preg_match('#function\s+([\w\\\\]+)#', (string)$method, $matches);
95 1
            $name = $matches[1];
96
        }
97
98 4
        return call_user_func_array($callback, $this->resolveParams($params, '!' . $name, $extra));
99
    }
100
101
    /**
102
     * @param string $className
103
     * @param array $extraParameters
104
     * @return object of $className«
105
     * @throws \Psr\Container\ContainerExceptionInterface
106
     * @throws \ReflectionException
107
     */
108 4
    public function produce(string $className, array $extraParameters = [])
109
    {
110 4
        if ($this->container->hasLocalEntry($className)) {
111
            return $this->container->get($className);
112
        }
113
114 4
        if (!class_exists($className)) {
115
            throw new \RuntimeException("$className not found");
116
        }
117 1
118 4
        $instance = $this->instantiate($className, $extraParameters);
119 1
120 4
        $this->container->set($className, $instance);
121 1
122 4
        return $instance;
123 1
    }
124 1
125 1
    /**
126 1
     * @param string $className
127 1
     * @return object of $className
128 1
     * @throws \Psr\Container\ContainerExceptionInterface
129 1
     * @throws \ReflectionException
130 1
     */
131 1
    public function getOrProduce(string $className)
132
    {
133 1
        $recipe = $this->container->getRecipe($className);
134
        if ($recipe) {
135 1
            return $this->container->get($className);
136
        }
137 1
        return $this->produce($className);
138 1
    }
139
140
    /**
141
     * @param string $className
142
     * @return object of $className
143
     * @throws \Psr\Container\ContainerExceptionInterface
144
     * @throws \ReflectionException
145
     */
146 3
    public function getOrInstantiate(string $className)
147
    {
148 3
        $recipe = $this->container->getRecipe($className);
149 3
        if ($recipe) {
150 1
            return $this->container->get($className);
151
        }
152 2
        return $this->instantiate($className);
153
    }
154
155
    /**
156
     * @param object $obj
157
     * @param array $extra
158
     * @throws \Psr\Container\ContainerExceptionInterface
159
     */
160 4
    public function inject($obj, array $extra = []): void
161
    {
162 4
        if (!$this->container->has(self::KEY_INJECTORS)) {
163 4
            return;
164
        }
165 1
        foreach ($this->container->get(self::KEY_INJECTORS) as $injector) {
166
            /**
167
             * @var InjectorInterface $injector
168
             */
169 1
            if ($injector->isTarget($obj)) {
170 1
                $injector->inject($this, $obj, $extra);
171
            }
172
        }
173 1
    }
174
175
    /**
176
     * @param $basename
177
     * @param array $keys
178
     * @param null|string $className
179
     * @param array $extra
180
     * @return mixed|object
181
     * @throws \Psr\Container\ContainerExceptionInterface
182
     * @throws \ReflectionException
183
     */
184 4
    public function resolveDependency($basename, array $keys, ?string $className = null, array $extra = [])
185
    {
186 4
        if ($value = $this->produceFromClass($basename, $keys, $extra)) {
187 3
            return $value[0];
188
        }
189
190 4
        if ($className) {
191 3
            return $this->getOrInstantiate($className);
192
        }
193
194 3
        throw new ContainerException('failed to produce dependency');
195
    }
196
197 1
    public function addInjector(InjectorInterface $injector): self
198
    {
199 1
        if (!$this->container->has(static::KEY_INJECTORS)) {
200 1
            $this->container->set(static::KEY_INJECTORS, [$injector]);
201
        } else {
202
            $this->container->set(
203
                static::KEY_INJECTORS,
204
                array_merge($this->container->get(static::KEY_INJECTORS), [$injector])
205
            );
206
        }
207
208 1
        return $this;
209
    }
210
211
    public function get($id)
212
    {
213
        if (!$this->has($id)) {
214
            throw new ContainerException('unknown class ' . $id);
215
        }
216
217
        return $this->getOrInstantiate($id);
218
    }
219
220
    public function has($id)
221
    {
222
        return class_exists($id);
223
    }
224
225
226
    /**
227
     * @param string $basename
228
     * @param array $keys
229
     * @param array $extra
230
     * @return array|null
231
     * @throws \Psr\Container\ContainerExceptionInterface
232
     */
233 4
    protected function produceFromClass(string $basename, array $keys, array $extra = [])
234
    {
235 4
        if (!empty($extra) && ($value = $this->findFromArray($extra, $keys))) {
236 3
            return $value;
237
        }
238 4
        $current = $basename;
239
240
        do {
241 4
            if (!empty($current)
242 4
                && $this->container->has("$current::")
243 4
                && ($value = $this->findFromArray($this->container->get("$current::"), $keys))
244
            ) {
245 2
                return $value;
246
            }
247 4
        } while ($current = get_parent_class($current));
248
249 4
        return null;
250
    }
251
252 4
    protected function findFromArray($arr, $keys)
253
    {
254 4
        foreach ($keys as $key) {
255 4
            if (array_key_exists($key, $arr)) {
256 4
                return [$this->container->resolveRecipe($arr[$key])];
257
            }
258
        }
259
260 2
        return null;
261
    }
262
263 6
    protected function resolveParams(array $params, string $basename, array $extra = [])
264
    {
265 6
        return array_map(
266 6
            function (\ReflectionParameter $parameter) use ($basename, $extra) {
267 4
                return $this->resolveParam($basename, $parameter, $extra);
268 6
            },
269 6
            $params
270
        );
271
    }
272
273
    /**
274
     * @param $basename
275
     * @param \ReflectionParameter $parameter
276
     * @param array $extraParameters
277
     * @return mixed|object
278
     * @throws \Psr\Container\ContainerExceptionInterface
279
     * @throws \ReflectionException
280
     */
281 4
    protected function resolveParam($basename, \ReflectionParameter $parameter, array $extraParameters)
282
    {
283 4
        $hash = sprintf('%s#%d', $basename, $parameter->getPosition());
284 4
        if (isset($this->circularStore[$hash])) {
285 1
            throw new CircularDependencyException(array_keys($this->circularStore));
286
        }
287
288
        try {
289 4
            $this->circularStore[$hash] = true;
290 4
            list($keys, $paramClassName) = $this->parseParameter($parameter);
291
292 4
            return $this->resolveDependency($basename, $keys, $paramClassName, $extraParameters);
293 4
        } catch (CircularDependencyException $e) {
294 1
            throw $e;
295 3
        } catch (ContainerException $e) {
296 3
            if ($parameter->isOptional()) {
297 2
                return $parameter->getDefaultValue();
298
            }
299
300 1
            throw new ContainerException(
301 1
                sprintf('failed to produce constructor parameter "%s" for %s', $parameter->getName(), $basename),
302 1
                0,
303 1
                $e
304
            );
305
        } finally {
306 4
            unset($this->circularStore[$hash]);
307
        }
308
    }
309
310
    /**
311
     * @param \ReflectionParameter $parameter
312
     * @return array
313
     */
314 4
    protected function parseParameter(\ReflectionParameter $parameter)
315
    {
316 4
        $paramClassName = null;
317 4
        $keys = [$parameter->name];
318
319
        try {
320 4
            $paramClass = $parameter->getClass();
321 4
            if (!empty($paramClass)) {
322 4
                $keys[] = $paramClassName = $paramClass->name;
323
            }
324
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (\ReflectionException $e) {
325
            //ignore exception when $parameter is type hinting for interface
326
        }
327
328 4
        $keys[] = $parameter->getPosition();
329
330 4
        return [$keys, $paramClassName];
331
    }
332
}
333