Passed
Branch master (4c70de)
by mcfog
02:49
created

Factory   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Test Coverage

Coverage 77.65%

Importance

Changes 0
Metric Value
wmc 51
eloc 119
dl 0
loc 309
ccs 132
cts 170
cp 0.7765
rs 7.92
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A of() 0 7 2
A instantiate() 0 13 2
A __construct() 0 10 3
A getOrInstantiate() 0 7 2
A resolveDependency() 0 15 6
A resolveParam() 0 26 5
A parseParameter() 0 17 3
A getOrProduce() 0 7 2
B produceFromClass() 0 17 7
A addInjector() 0 12 2
A findFromArray() 0 9 3
A resolveParams() 0 7 1
A invoke() 0 26 6
A inject() 0 11 4
A produce() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like Factory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Factory, and based on these observations, apply Extract Interface, too.

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
            assert(is_array($callback));
85 5
            $method = (new \ReflectionClass($callback[0]))->getMethod($callback[1]);
86 5
            $params = $method->getParameters();
87
        }
88
89 5
        if ($method->isClosure()) {
90 2
            $name = sprintf('Closure@%s:%d', $method->getFileName(), $method->getStartLine());
91 5
        } elseif ($method instanceof \ReflectionMethod) {
92 5
            $name = sprintf('%s::%s', $method->getDeclaringClass()->name, $method->name);
93
        } else {
94 1
            assert($method instanceof \ReflectionFunction);
95 1
            preg_match('#function\s+([\w\\\\]+)#', (string)$method, $matches);
96 1
            $name = $matches[1];
97
        }
98
99 5
        assert(is_callable($callback));
100 5
        return call_user_func_array($callback, $this->resolveParams($params, '!' . $name, $extra));
101
    }
102
103
    /**
104
     * @param string $className
105
     * @param array $extraParameters
106
     * @return object of $className«
107
     * @throws \Psr\Container\ContainerExceptionInterface
108
     * @throws \ReflectionException
109
     */
110 4
    public function produce(string $className, array $extraParameters = [])
111
    {
112 4
        if ($this->container->hasLocalEntry($className)) {
113
            return $this->container->get($className);
114
        }
115
116 5
        if (!class_exists($className)) {
117
            throw new \RuntimeException("$className not found");
118 1
        }
119 1
120 5
        $instance = $this->instantiate($className, $extraParameters);
121 1
122 4
        $this->container->set($className, $instance);
123 1
124 4
        return $instance;
125 1
    }
126 1
127 1
    /**
128 1
     * @param string $className
129 1
     * @return object of $className
130 1
     * @throws \Psr\Container\ContainerExceptionInterface
131
     * @throws \ReflectionException
132 1
     */
133
    public function getOrProduce(string $className)
134 1
    {
135
        $recipe = $this->container->getRecipe($className);
136 1
        if ($recipe) {
137 1
            return $this->container->get($className);
138
        }
139
        return $this->produce($className);
140
    }
141
142
    /**
143
     * @param string $className
144
     * @return object of $className
145
     * @throws \Psr\Container\ContainerExceptionInterface
146
     * @throws \ReflectionException
147
     */
148 1
    public function getOrInstantiate(string $className)
149
    {
150 1
        $recipe = $this->container->getRecipe($className);
151 1
        if ($recipe) {
152
            return $this->container->get($className);
153
        }
154 1
        return $this->instantiate($className);
155
    }
156
157
    /**
158
     * @param object $obj
159
     * @param array $extra
160
     * @throws \Psr\Container\ContainerExceptionInterface
161
     */
162 4
    public function inject($obj, array $extra = []): void
163
    {
164 4
        if (!$this->container->has(self::KEY_INJECTORS)) {
165 4
            return;
166
        }
167 1
        foreach ($this->container->get(self::KEY_INJECTORS) as $injector) {
168
            /**
169
             * @var InjectorInterface $injector
170
             */
171 1
            if ($injector->isTarget($obj)) {
172 1
                $injector->inject($this, $obj, $extra);
173
            }
174
        }
175 1
    }
176
177
    /**
178
     * @param string $basename
179
     * @param array $keys
180
     * @param null|string $className
181
     * @param array $extra
182
     * @return mixed|object
183
     * @throws \Psr\Container\ContainerExceptionInterface
184
     * @throws \ReflectionException
185
     */
186 4
    public function resolveDependency(string $basename, array $keys, ?string $className = null, array $extra = [])
187
    {
188 4
        if ($value = $this->produceFromClass($basename, $keys, $extra)) {
189 3
            return $value[0];
190
        }
191
192 4
        if ($className && $this->container->has($className)) {
193 2
            return $this->container->get($className);
194
        }
195
196 3
        if ($className && class_exists($className)) {
197 1
            return $this->getOrInstantiate($className);
198
        }
199
200 3
        throw new ContainerException('failed to produce dependency');
201
    }
202
203 1
    public function addInjector(InjectorInterface $injector): self
204
    {
205 1
        if (!$this->container->has(static::KEY_INJECTORS)) {
206 1
            $this->container->set(static::KEY_INJECTORS, [$injector]);
207
        } else {
208
            $this->container->set(
209
                static::KEY_INJECTORS,
210
                array_merge($this->container->get(static::KEY_INJECTORS), [$injector])
211
            );
212
        }
213
214 1
        return $this;
215
    }
216
217
    /**
218
     * @param string $basename
219
     * @param array $keys
220
     * @param array $extra
221
     * @return array|null
222
     * @throws \Psr\Container\ContainerExceptionInterface
223
     */
224 4
    protected function produceFromClass(string $basename, array $keys, array $extra = [])
225
    {
226 4
        if (!empty($extra) && ($value = $this->findFromArray($extra, $keys))) {
227 3
            return $value;
228
        }
229 4
        $current = $basename;
230
231
        do {
232 4
            if (!empty($current)
233 4
                && $this->container->has("$current::")
234 4
                && ($value = $this->findFromArray($this->container->get("$current::"), $keys))
235
            ) {
236 2
                return $value;
237
            }
238 4
        } while ($current = get_parent_class($current));
239
240 4
        return null;
241
    }
242
243 4
    protected function findFromArray($arr, $keys)
244
    {
245 4
        foreach ($keys as $key) {
246 4
            if (array_key_exists($key, $arr)) {
247 4
                return [$this->container->resolveRecipe($arr[$key])];
248
            }
249
        }
250
251 2
        return null;
252
    }
253
254 7
    protected function resolveParams(array $params, string $basename, array $extra = [])
255
    {
256 7
        return array_map(
257
            function (\ReflectionParameter $parameter) use ($basename, $extra) {
258 4
                return $this->resolveParam($basename, $parameter, $extra);
259 7
            },
260 7
            $params
261
        );
262
    }
263
264
    /**
265
     * @param string $basename
266
     * @param \ReflectionParameter $parameter
267
     * @param array $extraParameters
268
     * @return mixed|object
269
     * @throws \Psr\Container\ContainerExceptionInterface
270
     * @throws \ReflectionException
271
     */
272 4
    protected function resolveParam($basename, \ReflectionParameter $parameter, array $extraParameters)
273
    {
274 4
        $hash = sprintf('%s#%d', $basename, $parameter->getPosition());
275 4
        if (isset($this->circularStore[$hash])) {
276 1
            throw new CircularDependencyException(array_keys($this->circularStore));
277
        }
278
279
        try {
280 4
            $this->circularStore[$hash] = true;
281 4
            list($keys, $paramClassName) = $this->parseParameter($parameter);
282
283 4
            return $this->resolveDependency($basename, $keys, $paramClassName, $extraParameters);
284 4
        } catch (CircularDependencyException $e) {
285 1
            throw $e;
286 3
        } catch (ContainerException $e) {
287 3
            if ($parameter->isOptional()) {
288 3
                return $parameter->getDefaultValue();
289
            }
290
291 1
            throw new ContainerException(
292 1
                sprintf('failed to produce constructor parameter "%s" for %s', $parameter->getName(), $basename),
293 1
                0,
294 1
                $e
295
            );
296
        } finally {
297 4
            unset($this->circularStore[$hash]);
298
        }
299
    }
300
301
    /**
302
     * @param \ReflectionParameter $parameter
303
     * @return array
304
     */
305 4
    protected function parseParameter(\ReflectionParameter $parameter)
306
    {
307 4
        $paramClassName = null;
308 4
        $keys = [$parameter->name];
309
310
        try {
311 4
            $paramClass = $parameter->getClass();
312 4
            if (!empty($paramClass)) {
313 4
                $keys[] = $paramClassName = $paramClass->name;
314
            }
315
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (\ReflectionException $e) {
316
            //ignore exception when $parameter is type hinting for interface
317
        }
318
319 4
        $keys[] = $parameter->getPosition();
320
321 4
        return [$keys, $paramClassName];
322
    }
323
}
324