Completed
Branch master (daf1a7)
by Anton
01:13
created

Builder::construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php declare(strict_types=1);
2
3
namespace Phact\Container\Builder;
4
5
use Phact\Container\Definition\DefinitionInterface;
6
use Phact\Container\Details\CallInterface;
7
use Phact\Container\Details\PropertyInterface;
8
use Phact\Container\Exceptions\InvalidConfigurationException;
9
use Phact\Container\Exceptions\InvalidFactoryException;
10
use Phact\Container\Exceptions\NotFoundException;
11
use Phact\Container\Inflection\InflectionInterface;
12
use Psr\Container\ContainerInterface;
13
14
class Builder implements BuilderInterface
15
{
16
    /**
17
     * @var ContainerInterface
18
     */
19
    protected $container;
20
21
    /**
22
     * @var DependenciesResolver
23
     */
24
    protected $dependenciesResolver;
25
26
    protected $autoWire = true;
27
28 61
    public function __construct(bool $autoWire = true, ?DependenciesResolverInterface $dependenciesResolver = null)
29
    {
30 61
        $this->autoWire = $autoWire;
31 61
        $this->dependenciesResolver = $dependenciesResolver ?: new DependenciesResolver();
32 61
    }
33
34
    /**
35
     * {@inheritDoc}
36
     */
37 48
    public function setContainer(ContainerInterface $container): void
38
    {
39 48
        $this->container = $container;
40 48
    }
41
42
    /**
43
     * {@inheritDoc}
44
     */
45 39
    public function construct(DefinitionInterface $definition): object
46
    {
47 39
        if ($definition->getFactory()) {
48 12
            $object = $this->makeObjectWithFactory($definition);
49
        } else {
50 27
            $object = $this->makeObjectSelf($definition);
51
        }
52 33
        return $object;
53
    }
54
55
    /**
56
     * {@inheritDoc}
57
     */
58 10
    public function configure(object $object, DefinitionInterface $definition): object
59
    {
60 10
        $this->applyProperties($object, $definition->getProperties());
61 10
        $this->applyCalls($object, $definition->getCalls());
62 10
        return $object;
63
    }
64
65
    /**
66
     * {@inheritDoc}
67
     */
68 1
    public function invoke(callable $callable, array $arguments = [])
69
    {
70 1
        return $this->call($callable, $arguments);
71
    }
72
73
    /**
74
     * {@inheritDoc}
75
     */
76 2
    public function inflect(object $object, InflectionInterface $inflection): object
77
    {
78 2
        $this->applyCalls($object, $inflection->getCalls());
79 2
        $this->applyProperties($object, $inflection->getProperties());
80 2
        return $object;
81
    }
82
83
    /**
84
     * @param object $object
85
     * @param PropertyInterface[] $properties
86
     */
87 12
    protected function applyProperties(object $object, array $properties): void
88
    {
89 12
        foreach ($properties as $property) {
90 2
            $object->{$property->getName()} = $property->getValue();
91
        }
92 12
    }
93
94
    /**
95
     * @param object $object
96
     * @param CallInterface[] $calls
97
     * @throws NotFoundException
98
     */
99 12
    protected function applyCalls(object $object, array $calls): void
100
    {
101 12
        foreach ($calls as $call) {
102 4
            $this->call([$object, $call->getMethod()], $call->getArguments());
103
        }
104 12
    }
105
106
    /**
107
     * @param callable $callable
108
     * @param array $arguments
109
     * @return mixed
110
     * @throws NotFoundException
111
     */
112 5
    protected function call(callable $callable, array $arguments = [])
113
    {
114 5
        $dependencies = [];
115 5
        if ($this->autoWire) {
116 5
            $dependencies = $this->dependenciesResolver->resolveCallableDependencies($callable);
117
        }
118 5
        $parameters = $this->buildParameters($arguments);
119 5
        $args = $this->buildArguments($dependencies, $parameters);
120 5
        return call_user_func_array($callable, $args);
121
    }
122
123
    /**
124
     * Make object without factory
125
     *
126
     * @param DefinitionInterface $definition
127
     * @return mixed
128
     * @throws NotFoundException
129
     */
130 27
    protected function makeObjectSelf(DefinitionInterface $definition)
131
    {
132 27
        $dependencies = [];
133
134 27
        $className = $definition->getClass();
135 27
        if ($this->autoWire) {
136 26
            $dependencies = $this->dependenciesResolver->resolveConstructorDependencies(
137 26
                $className,
138 26
                $definition->getConstructMethod()
139
            );
140
        }
141 27
        $parameters = $this->buildParameters($definition->getArguments());
142 27
        $arguments = $this->buildArguments($dependencies, $parameters);
143
144 24
        return $this->constructObject($definition->getClass(), $arguments, $definition->getConstructMethod());
145
    }
146
147
    /**
148
     * Make object with factory
149
     *
150
     * @param DefinitionInterface $definition
151
     * @return mixed
152
     * @throws InvalidConfigurationException
153
     * @throws InvalidFactoryException
154
     * @throws NotFoundException
155
     */
156 12
    protected function makeObjectWithFactory(DefinitionInterface $definition)
157
    {
158 12
        $factory = $definition->getFactory();
159
160 12
        if (!is_callable($factory) || is_array($factory)) {
161 9
            $factory = $this->buildFactoryFromNonCallable($definition);
162
        }
163
164 9
        $dependencies = [];
165 9
        if ($this->autoWire) {
166 9
            $dependencies = $this->dependenciesResolver->resolveCallableDependencies($factory);
167
        }
168 9
        $parameters = $this->buildParameters($definition->getArguments());
169 9
        $arguments = $this->buildArguments($dependencies, $parameters);
170 9
        return call_user_func_array($factory, $arguments);
171
    }
172
173
    /**
174
     * Build callable factory from non-callable
175
     *
176
     * @param DefinitionInterface $definition
177
     * @return callable
178
     * @throws InvalidConfigurationException
179
     * @throws InvalidFactoryException
180
     */
181 9
    protected function buildFactoryFromNonCallable(DefinitionInterface $definition): callable
182
    {
183 9
        if (!$this->container) {
184 1
            throw new InvalidConfigurationException('Please, provide container for usage non-callable factories');
185
        }
186 8
        $factory = $definition->getFactory();
187 8
        $factoryId = null;
0 ignored issues
show
Unused Code introduced by
$factoryId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
188 8
        $factoryMethod = null;
0 ignored issues
show
Unused Code introduced by
$factoryMethod is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
189 8
        if (is_string($factory)) {
190 5
            $factoryId = $this->fetchDependencyId($factory);
191 5
            $factoryMethod = $definition->getConstructMethod() ?: '__invoke';
192 3
        } elseif (is_array($factory)) {
193 2
            $factoryId = $this->fetchDependencyId($factory[0]);
194 2
            $factoryMethod = $factory[1];
195
        } else {
196 1
            throw new InvalidFactoryException('Incorrect factory provided, available string and array factories');
197
        }
198 7
        if ($this->container->has($factoryId)) {
199 6
            $factoryResolved = $this->container->get($factoryId);
200 6
            return [$factoryResolved, $factoryMethod];
201
        }
202 1
        throw new InvalidFactoryException('Incorrect factory provided');
203
    }
204
205
    /**
206
     * Create object with provided arguments and optional construct method
207
     *
208
     * @param string $className
209
     * @param array $arguments
210
     * @param string|null $constructMethod
211
     * @return mixed
212
     */
213 24
    protected function constructObject(string $className, array $arguments, ?string $constructMethod = null)
214
    {
215 24
        if ($constructMethod !== null) {
216 3
            $obj = $className::$constructMethod(...$arguments);
217
        } else {
218 21
            $obj = new $className(...$arguments);
219
        }
220 24
        return $obj;
221
    }
222
223
    /**
224
     * Build function attributes for type-value representation
225
     *
226
     * @param array $attributes
227
     * @return Parameter[]
228
     */
229 41
    protected function buildParameters($attributes = []): array
230
    {
231 41
        $parameters = [];
232 41
        foreach ($attributes as $key => $attribute) {
233 18
            $parameters[$key] = $this->buildParameter($attribute);
234
        }
235 41
        return $parameters;
236
    }
237
238
    /**
239
     * Create parameter from provided argument
240
     *
241
     * @param $value
242
     * @return ParameterInterface
243
     */
244 18
    protected function buildParameter($value): ParameterInterface
245
    {
246 18
        $type = ParameterInterface::TYPE_VALUE;
247 18
        if (\is_string($value) && 0 === strpos($value, '@')) {
248 6
            $type = ParameterInterface::TYPE_REFERENCE_REQUIRED;
249 6
            if (0 === strpos($value, '@?')) {
250 2
                $value = substr($value, 2);
251 2
                $type = ParameterInterface::TYPE_REFERENCE_OPTIONAL;
252 4
            } elseif (0 === strpos($value, '@@')) {
253 1
                $value = substr($value, 1);
254 1
                $type = DependencyInterface::TYPE_VALUE;
255
            } else {
256 3
                $value = substr($value, 1);
257
            }
258
        }
259 18
        return new Parameter($type, $value);
260
    }
261
262
    /**
263
     * Try fetch dependency name from string value
264
     *
265
     * @param string $value
266
     * @return string|null
267
     */
268 7
    protected function fetchDependencyId(string $value): ?string
269
    {
270 7
        if (0 === strpos($value, '@')) {
271 3
            return substr($value, 1);
272
        }
273 4
        return $value;
274
    }
275
276
    /**
277
     * Build arguments by dependencies and parameters
278
     *
279
     * @param DependencyInterface[] $dependencies
280
     * @param ParameterInterface[] $parameters
281
     * @return array
282
     * @throws NotFoundException
283
     */
284 41
    protected function buildArguments(array $dependencies, array $parameters): array
285
    {
286 41
        $arguments = [];
287 41
        if (count($dependencies) > 0) {
288 33
            $arguments = $this->buildArgumentsFromDependencies($dependencies, $parameters);
289
        } else {
290 10
            foreach ($parameters as $parameter) {
291 1
                $arguments[] = $this->makeArgumentByParameter($parameter);
292
            }
293
        }
294 38
        return $arguments;
295
    }
296
297
    /**
298
     * @param DependencyInterface[] $dependencies
299
     * @param ParameterInterface[] $parameters
300
     * @return array
301
     * @throws NotFoundException
302
     */
303 33
    protected function buildArgumentsFromDependencies(array $dependencies, array $parameters): array
304
    {
305 33
        $arguments = [];
306 33
        $usedParameters = [];
307
308 33
        foreach ($dependencies as $key => $dependency) {
309
            /** @var ParameterInterface $parameter */
310 33
            $parameter = null;
0 ignored issues
show
Unused Code introduced by
$parameter is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
311 33
            if (isset($parameters[$key])) {
312 7
                $parameter = $parameters[$key];
313 7
                $usedParameters[] = $key;
314 7
                $arguments[] = $this->makeArgumentByParameter($parameter);
315 26
            } elseif (isset($parameters[$dependency->getName()])) {
316 10
                $parameter = $parameters[$dependency->getName()];
317 10
                $usedParameters[] = $dependency->getName();
318 10
                $arguments[] = $this->makeArgumentByParameter($parameter);
319
            } else {
320 25
                $arguments[] = $this->makeArgumentByDependency($dependency);
321
            }
322
        }
323
324 30
        $arguments = $this->appendUnusedParamsToArguments($parameters, $usedParameters, $arguments);
325
326 30
        return $arguments;
327
    }
328
329
    /**
330
     * @param array $parameters
331
     * @param array $usedParameters
332
     * @param array $arguments
333
     * @return array
334
     * @throws NotFoundException
335
     */
336 30
    protected function appendUnusedParamsToArguments(array $parameters, array $usedParameters, array $arguments): array
337
    {
338 30
        foreach ($parameters as $key => $parameter) {
339 16
            if (!in_array($key, $usedParameters, true)) {
340 1
                $arguments[] = $this->makeArgumentByParameter($parameter);
341
            }
342
        }
343 30
        return $arguments;
344
    }
345
346
    /**
347
     * @param ParameterInterface $parameter
348
     * @return mixed
349
     * @throws NotFoundException
350
     */
351 18
    protected function makeArgumentByParameter(ParameterInterface $parameter)
352
    {
353 18
        switch ($parameter->getType()) {
354 18
            case ParameterInterface::TYPE_REFERENCE_REQUIRED:
355 3
                $resolved = $this->retrieveDependencyFromContainer($parameter->getValue());
356 3
                if ($resolved) {
357 2
                    return $resolved;
358
                }
359 1
                throw new NotFoundException("There is no referenced classes of {$parameter->getValue()} found");
360
361 16
            case ParameterInterface::TYPE_REFERENCE_OPTIONAL:
362 2
                $resolved = $this->retrieveDependencyFromContainer($parameter->getValue());
363 2
                if ($resolved) {
364 1
                    return $resolved;
365
                }
366 1
                return null;
367
        }
368 14
        return $parameter->getValue();
369
    }
370
371
    /**
372
     * @param DependencyInterface $dependency
373
     * @return mixed
374
     * @throws NotFoundException
375
     */
376 25
    protected function makeArgumentByDependency(DependencyInterface $dependency)
377
    {
378 25
        switch ($dependency->getType()) {
379 25
            case DependencyInterface::TYPE_REQUIRED:
380 20
                $resolved = $this->retrieveDependencyFromContainer($dependency->getValue());
381 19
                if ($resolved) {
382 18
                    return $resolved;
383
                }
384 1
                throw new NotFoundException("There is no referenced classes of {$dependency->getValue()} found");
385
386 10
            case DependencyInterface::TYPE_OPTIONAL:
387 2
                $resolved = $this->retrieveDependencyFromContainer($dependency->getValue());
388 2
                if ($resolved) {
389 1
                    return $resolved;
390
                }
391 1
                return null;
392
        }
393 8
        return $dependency->getValue();
394
    }
395
396
    /**
397
     * Fetch dependency from container
398
     *
399
     * @param $value
400
     * @return mixed|null
401
     */
402 27
    protected function retrieveDependencyFromContainer($value)
403
    {
404 27
        if ($this->container && $this->container->has($value)) {
405 23
            return $this->container->get($value);
406
        }
407 4
        return null;
408
    }
409
}
410