Passed
Push — master ( 02eddd...2aa0cc )
by mcfog
02:49 queued 18s
created

Factory::resolveBasedOnConsumer()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

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