Factory::of()   A
last analyzed

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
/**
14
 * Air DI Factory
15
 */
16
class Factory
17
{
18
    public const CONTAINER_KEY = self::class;
19
    public const INJECTOR = self::class . '::injector';
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(static::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(static::CONTAINER_KEY)) {
50 10
            return new static($container);
51
        }
52
53 3
        return $container->get(static::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 5
    public function instantiate(string $className, array $extraParameters = [])
66
    {
67 5
        $class = new \ReflectionClass($className);
68 6
        $constructor = $class->getConstructor();
69 1
70 6
        $constructParams = $constructor
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
71 3
            ? $this->resolveParams($constructor->getParameters(), $className, $extraParameters)
72 4
            : [];
73
74 4
        $instance = $class->newInstanceArgs($constructParams);
75 4
        $this->inject($instance, $extraParameters);
76
77 4
        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 6
    public function invoke(callable $callback, array $extra = [])
88
    {
89 6
        if (is_string($callback) || $callback instanceof \Closure) {
90 3
            $method = new \ReflectionFunction($callback);
91 3
            $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 6
        if ($method->isClosure()) {
102 3
            $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 6
        assert(is_callable($callback));
112 6
        return $callback(...$this->resolveParams($params, '!' . $name, $extra));
113
    }
114
115
    /**
116 1
     * Produce a $className instance.
117
     *
118 1
     * @param string $className Requested classname.
119 1
     * @param array  $extra     Extra parameters.
120 1
     * @param bool   $cached    Whether to save the instance if it's not defined in container.
121 1
     * @return object of $className
122 1
     * @throws \ReflectionException Failure during reflection process.
123 1
     */
124 4
    public function produce(string $className, array $extra = [], bool $cached = true)
125 1
    {
126 4
        if (!class_exists($className)) {
127 1
            throw new \RuntimeException("$className not found");
128 1
        }
129 1
130 4
        if ($this->container->has($className)) {
131
            return $this->container->get($className);
132 1
        }
133
134 4
        $instance = $this->instantiate($className, $extra);
135 3
        if ($cached) {
136 4
            $this->container->set($className, $instance);
137 1
        }
138
139 3
        return $instance;
140
    }
141
142
    /**
143
     * Run injectors on object. (Typically for setter injection)
144
     *
145
     * @param object $obj   The object to be injected.
146
     * @param array  $extra Extra parameters.
147
     * @return void
148
     * @throws \Psr\Container\ContainerExceptionInterface Failure when fetching dependency in container.
149
     */
150 4
    protected function inject($obj, array $extra = []): void
151
    {
152 4
        if (!$this->container->has(static::INJECTOR)) {
153 4
            return;
154
        }
155
156
        /**
157
         * @var InjectorInterface $injector
158
         */
159 1
        $injector = $this->container->get(static::INJECTOR);
160 1
        $injector->inject($this, $obj, $extra);
161 1
    }
162
163
    /**
164
     * Resolve a dependency item.
165
     * http://litphp.github.io/docs/air-di#working-on-dependencies
166
     *
167
     * @param string      $consumer  Represents who need the dependency. Often a class name.
168
     * @param array       $keys      Ordered array of string keys describing required dependency.
169
     * @param null|string $className Optional class name of the dependency.
170
     * @param array       $extra     Extra parameters.
171
     * @return mixed|object
172
     * @throws \Psr\Container\ContainerExceptionInterface Failure when fetching dependency in container.
173
     * @throws \ReflectionException Failure during reflection process.
174
     */
175 5
    public function resolveDependency(string $consumer, array $keys, ?string $className = null, array $extra = [])
176
    {
177 5
        if ($value = $this->resolveBasedOnConsumer($consumer, $keys, $extra)) {
178 3
            return $value[0];
179
        }
180
181 5
        if ($className && $this->container->has($className)) {
182 3
            return $this->container->get($className);
183
        }
184
185 3
        if ($className && class_exists($className)) {
186 1
            return $this->instantiate($className, $extra);
187
        }
188
189 3
        throw new ContainerException('failed to produce dependency for ' . $consumer);
190
    }
191
192
    /**
193
     * A subprocess of dependency resolving: try use candicate keys and consumer to find dependency.
194
     * http://litphp.github.io/docs/air-di#working-on-dependencies
195
     *
196
     * @param string $consumer Represents who need the dependency. Often a class name.
197
     * @param array  $keys     Ordered array of string keys describing the dependency.
198
     * @param array  $extra    Extra parameters.
199
     * @return array|null return single element array when success, null when fail, so null value can be handled
200
     * @throws \Psr\Container\ContainerExceptionInterface Failure when fetching dependency in container.
201
     */
202 5
    protected function resolveBasedOnConsumer(string $consumer, array $keys, array $extra = [])
203
    {
204 5
        if (!empty($extra) && ($value = $this->findFromArray($extra, $keys))) {
205 3
            return $value;
206
        }
207 5
        $current = $consumer;
208
209
        do {
210
            if (
211 5
                !empty($current)
212 5
                && $this->container->has("$current::")
213 5
                && ($value = $this->findFromArray($this->container->get("$current::"), $keys))
214
            ) {
215 2
                return $value;
216
            }
217 5
        } while ($current = get_parent_class($current));
218
219 5
        return null;
220
    }
221
222 4
    protected function findFromArray($arr, $keys)
223
    {
224 4
        foreach ($keys as $key) {
225 4
            if (array_key_exists($key, $arr)) {
226 4
                return [$this->container->resolveRecipe($arr[$key])];
227
            }
228
        }
229
230 2
        return null;
231
    }
232
233
    /**
234
     * Resolve array of ReflectionParameter into concrete values
235
     *
236
     * @param array  $params   Array of ReflectionParameter.
237
     * @param string $consumer Represents who need the dependency.
238
     * @param array  $extra    Extra parameters.
239
     * @return array
240
     */
241 8
    protected function resolveParams(array $params, string $consumer, array $extra = [])
242
    {
243 8
        return array_map(
244
            function (\ReflectionParameter $parameter) use ($consumer, $extra) {
245 5
                return $this->resolveParam($consumer, $parameter, $extra);
246 8
            },
247 8
            $params
248
        );
249
    }
250
251
    /**
252
     * Resolve a parameter (of callback, or constructor)
253
     *
254
     * @param string               $consumer  Represents who need the dependency.
255
     * @param \ReflectionParameter $parameter The ReflectionParameter.
256
     * @param array                $extra     Extra parameters.
257
     * @return mixed|object
258
     * @throws \Psr\Container\ContainerExceptionInterface Failure when fetching dependency in container.
259
     * @throws \ReflectionException Failure during reflection process.
260
     */
261 5
    protected function resolveParam(string $consumer, \ReflectionParameter $parameter, array $extra)
262
    {
263 5
        $hash = sprintf('%s#%d', $consumer, $parameter->getPosition());
264 5
        if (isset($this->circularStore[$hash])) {
265 1
            throw new CircularDependencyException(array_keys($this->circularStore));
266
        }
267
268
        try {
269 5
            $this->circularStore[$hash] = true;
270 5
            [$keys, $paramClassName] = Factory::parseParameter($parameter);
271
272 5
            return $this->resolveDependency($consumer, $keys, $paramClassName, $extra);
273 4
        } catch (CircularDependencyException $e) {
274 1
            throw $e;
275 3
        } catch (ContainerException $e) {
276 3
            if ($parameter->isOptional()) {
277 3
                return $parameter->getDefaultValue();
278
            }
279
280 1
            throw new ContainerException(
281 1
                sprintf('failed to produce constructor parameter "%s" for %s', $parameter->getName(), $consumer),
282 1
                0,
283 1
                $e
284
            );
285
        } finally {
286 5
            unset($this->circularStore[$hash]);
287
        }
288
    }
289
290 5
    protected static function parseParameter(\ReflectionParameter $parameter)
291
    {
292 5
        $paramClassName = null;
293 5
        $keys = [$parameter->name];
294
295 5
        $paramClass = $parameter->getClass();
296 5
        if (!empty($paramClass)) {
297 5
            $keys[] = $paramClassName = $paramClass->name;
298
        }
299
300 5
        $keys[] = $parameter->getPosition();
301
302 5
        return [$keys, $paramClassName];
303
    }
304
}
305