Passed
Push — master ( 60ec40...f0d439 )
by mcfog
01:27
created

Factory::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.944

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 10
ccs 4
cts 10
cp 0.4
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 4.944
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 1
    public function getOrInstantiate(string $className)
147
    {
148 1
        $recipe = $this->container->getRecipe($className);
149 1
        if ($recipe) {
150
            return $this->container->get($className);
151
        }
152 1
        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 && $this->container->has($className)) {
191 2
            return $this->container->get($className);
192
        }
193
194 3
        if ($className && class_exists($className)) {
195 1
            return $this->getOrInstantiate($className);
196
        }
197
198 3
        throw new ContainerException('failed to produce dependency');
199
    }
200
201 1
    public function addInjector(InjectorInterface $injector): self
202
    {
203 1
        if (!$this->container->has(static::KEY_INJECTORS)) {
204 1
            $this->container->set(static::KEY_INJECTORS, [$injector]);
205
        } else {
206
            $this->container->set(
207
                static::KEY_INJECTORS,
208
                array_merge($this->container->get(static::KEY_INJECTORS), [$injector])
209
            );
210
        }
211
212 1
        return $this;
213
    }
214
215
    public function get($id)
216
    {
217
        if (!$this->has($id)) {
218
            throw new ContainerException('unknown class ' . $id);
219
        }
220
221
        return $this->getOrInstantiate($id);
222
    }
223
224
    public function has($id)
225
    {
226
        return class_exists($id);
227
    }
228
229
230
    /**
231
     * @param string $basename
232
     * @param array $keys
233
     * @param array $extra
234
     * @return array|null
235
     * @throws \Psr\Container\ContainerExceptionInterface
236
     */
237 4
    protected function produceFromClass(string $basename, array $keys, array $extra = [])
238
    {
239 4
        if (!empty($extra) && ($value = $this->findFromArray($extra, $keys))) {
240 3
            return $value;
241
        }
242 4
        $current = $basename;
243
244
        do {
245 4
            if (!empty($current)
246 4
                && $this->container->has("$current::")
247 4
                && ($value = $this->findFromArray($this->container->get("$current::"), $keys))
248
            ) {
249 2
                return $value;
250
            }
251 4
        } while ($current = get_parent_class($current));
252
253 4
        return null;
254
    }
255
256 4
    protected function findFromArray($arr, $keys)
257
    {
258 4
        foreach ($keys as $key) {
259 4
            if (array_key_exists($key, $arr)) {
260 4
                return [$this->container->resolveRecipe($arr[$key])];
261
            }
262
        }
263
264 2
        return null;
265
    }
266
267 6
    protected function resolveParams(array $params, string $basename, array $extra = [])
268
    {
269 6
        return array_map(
270 6
            function (\ReflectionParameter $parameter) use ($basename, $extra) {
271 4
                return $this->resolveParam($basename, $parameter, $extra);
272 6
            },
273 6
            $params
274
        );
275
    }
276
277
    /**
278
     * @param $basename
279
     * @param \ReflectionParameter $parameter
280
     * @param array $extraParameters
281
     * @return mixed|object
282
     * @throws \Psr\Container\ContainerExceptionInterface
283
     * @throws \ReflectionException
284
     */
285 4
    protected function resolveParam($basename, \ReflectionParameter $parameter, array $extraParameters)
286
    {
287 4
        $hash = sprintf('%s#%d', $basename, $parameter->getPosition());
288 4
        if (isset($this->circularStore[$hash])) {
289 1
            throw new CircularDependencyException(array_keys($this->circularStore));
290
        }
291
292
        try {
293 4
            $this->circularStore[$hash] = true;
294 4
            list($keys, $paramClassName) = $this->parseParameter($parameter);
295
296 4
            return $this->resolveDependency($basename, $keys, $paramClassName, $extraParameters);
297 4
        } catch (CircularDependencyException $e) {
298 1
            throw $e;
299 3
        } catch (ContainerException $e) {
300 3
            if ($parameter->isOptional()) {
301 3
                return $parameter->getDefaultValue();
302
            }
303
304 1
            throw new ContainerException(
305 1
                sprintf('failed to produce constructor parameter "%s" for %s', $parameter->getName(), $basename),
306 1
                0,
307 1
                $e
308
            );
309
        } finally {
310 4
            unset($this->circularStore[$hash]);
311
        }
312
    }
313
314
    /**
315
     * @param \ReflectionParameter $parameter
316
     * @return array
317
     */
318 4
    protected function parseParameter(\ReflectionParameter $parameter)
319
    {
320 4
        $paramClassName = null;
321 4
        $keys = [$parameter->name];
322
323
        try {
324 4
            $paramClass = $parameter->getClass();
325 4
            if (!empty($paramClass)) {
326 4
                $keys[] = $paramClassName = $paramClass->name;
327
            }
328
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (\ReflectionException $e) {
329
            //ignore exception when $parameter is type hinting for interface
330
        }
331
332 4
        $keys[] = $parameter->getPosition();
333
334 4
        return [$keys, $paramClassName];
335
    }
336
}
337