Completed
Push — master ( 338bba...3a4b59 )
by mcfog
03:00
created

Factory::of()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.3149

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 7
cp 0.5714
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.3149
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
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 8
    public function __construct(ContainerInterface $container = null)
27
    {
28 8
        if ($container instanceof Container) {
29 8
            $this->container = $container;
30
        } elseif (is_null($container)) {
31
            $this->container = new Container();
32
        } else {
33
            $this->container = Container::wrap($container);
34
        }
35 23
        $this->container->set(self::CONTAINER_KEY, $this);
36 8
    }
37 15
38 9
    public static function of(ContainerInterface $container): self
39
    {
40 8
        if (!$container->has(self::CONTAINER_KEY)) {
41 23
            return new self($container);
42
        }
43
44 3
        return $container->get(self::CONTAINER_KEY);
45
    }
46
47 1
    /**
48
     * @param string $className
49 1
     * @param array $extraParameters
50 1
     * @return object
51
     * @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 4
            ? $this->resolveParams($constructor->getParameters(), $className, $extraParameters)
61 4
            : [];
62 1
63 4
        $instance = $class->newInstanceArgs($constructParams);
64 4
        $this->inject($instance, $extraParameters);
65
66 4
        return $instance;
67
    }
68 1
69 1
    /**
70 1
     * @param callable $callback
71
     * @param array $extra
72
     * @return mixed
73
     * @throws \ReflectionException
74
     */
75 5
    public function invoke(callable $callback, array $extra = [])
76
    {
77 5
        if (is_string($callback) || $callback instanceof \Closure) {
78 2
            $method = new \ReflectionFunction($callback);
79 2
            $params = $method->getParameters();
80
        } else {
81 5
            if (is_object($callback)) {
82 1
                $callback = [$callback, '__invoke'];
83
            }
84 5
            $method = (new \ReflectionClass($callback[0]))->getMethod($callback[1]);
85 5
            $params = $method->getParameters();
86
        }
87
88 5
        if ($method->isClosure()) {
89 2
            $name = sprintf('Closure@%s:%d', $method->getFileName(), $method->getStartLine());
90 5
        } elseif ($method instanceof \ReflectionMethod) {
91 5
            $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 5
        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 1
        }
117
118 5
        $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
    public function getOrProduce(string $className)
132 1
    {
133
        $recipe = $this->container->getRecipe($className);
134 1
        if ($recipe) {
135
            return $this->container->get($className);
136 1
        }
137 1
        return $this->produce($className);
138
    }
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 string $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(string $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
    /**
216
     * @param string $basename
217
     * @param array $keys
218
     * @param array $extra
219
     * @return array|null
220
     * @throws \Psr\Container\ContainerExceptionInterface
221
     */
222 4
    protected function produceFromClass(string $basename, array $keys, array $extra = [])
223
    {
224 4
        if (!empty($extra) && ($value = $this->findFromArray($extra, $keys))) {
225 3
            return $value;
226
        }
227 4
        $current = $basename;
228
229
        do {
230 4
            if (!empty($current)
231 4
                && $this->container->has("$current::")
232 4
                && ($value = $this->findFromArray($this->container->get("$current::"), $keys))
233
            ) {
234 2
                return $value;
235
            }
236 4
        } while ($current = get_parent_class($current));
237
238 4
        return null;
239
    }
240
241 4
    protected function findFromArray($arr, $keys)
242
    {
243 4
        foreach ($keys as $key) {
244 4
            if (array_key_exists($key, $arr)) {
245 4
                return [$this->container->resolveRecipe($arr[$key])];
246
            }
247
        }
248
249 2
        return null;
250
    }
251
252 7
    protected function resolveParams(array $params, string $basename, array $extra = [])
253
    {
254 7
        return array_map(
255
            function (\ReflectionParameter $parameter) use ($basename, $extra) {
256 4
                return $this->resolveParam($basename, $parameter, $extra);
257 7
            },
258 7
            $params
259
        );
260
    }
261
262
    /**
263
     * @param string $basename
264
     * @param \ReflectionParameter $parameter
265
     * @param array $extraParameters
266
     * @return mixed|object
267
     * @throws \Psr\Container\ContainerExceptionInterface
268
     * @throws \ReflectionException
269
     */
270 4
    protected function resolveParam($basename, \ReflectionParameter $parameter, array $extraParameters)
271
    {
272 4
        $hash = sprintf('%s#%d', $basename, $parameter->getPosition());
273 4
        if (isset($this->circularStore[$hash])) {
274 1
            throw new CircularDependencyException(array_keys($this->circularStore));
275
        }
276
277
        try {
278 4
            $this->circularStore[$hash] = true;
279 4
            list($keys, $paramClassName) = $this->parseParameter($parameter);
280
281 4
            return $this->resolveDependency($basename, $keys, $paramClassName, $extraParameters);
282 4
        } catch (CircularDependencyException $e) {
283 1
            throw $e;
284 3
        } catch (ContainerException $e) {
285 3
            if ($parameter->isOptional()) {
286 3
                return $parameter->getDefaultValue();
287
            }
288
289 1
            throw new ContainerException(
290 1
                sprintf('failed to produce constructor parameter "%s" for %s', $parameter->getName(), $basename),
291 1
                0,
292 1
                $e
293
            );
294
        } finally {
295 4
            unset($this->circularStore[$hash]);
296
        }
297
    }
298
299
    /**
300
     * @param \ReflectionParameter $parameter
301
     * @return array
302
     */
303 4
    protected function parseParameter(\ReflectionParameter $parameter)
304
    {
305 4
        $paramClassName = null;
306 4
        $keys = [$parameter->name];
307
308
        try {
309 4
            $paramClass = $parameter->getClass();
310 4
            if (!empty($paramClass)) {
311 4
                $keys[] = $paramClassName = $paramClass->name;
312
            }
313
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (\ReflectionException $e) {
314
            //ignore exception when $parameter is type hinting for interface
315
        }
316
317 4
        $keys[] = $parameter->getPosition();
318
319 4
        return [$keys, $paramClassName];
320
    }
321
}
322