Builder::constructObject()   A
last analyzed

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 3
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
    /**
27
     * @var ArgumentsBuilder
28
     */
29
    protected $argumentsBuilder;
30
31
    protected $analyzeArguments = true;
32
33 61
    public function __construct(
34
        bool $analyzeArguments = true,
35
        ?DependenciesResolverInterface $dependenciesResolver = null
36
    ) {
37 61
        $this->analyzeArguments = $analyzeArguments;
38 61
        $this->dependenciesResolver = $dependenciesResolver ?: new DependenciesResolver();
39 61
        $this->argumentsBuilder = new ArgumentsBuilder();
40 61
    }
41
42
    /**
43
     * {@inheritDoc}
44
     */
45 48
    public function setContainer(ContainerInterface $container): void
46
    {
47 48
        $this->container = $container;
48 48
        $this->argumentsBuilder->setContainer($container);
49 48
    }
50
51
    /**
52
     * {@inheritDoc}
53
     */
54 39
    public function construct(DefinitionInterface $definition): object
55
    {
56 39
        if ($definition->getFactory()) {
57 12
            $object = $this->makeObjectWithFactory($definition);
58
        } else {
59 27
            $object = $this->makeObjectSelf($definition);
60
        }
61 33
        return $object;
62
    }
63
64
    /**
65
     * {@inheritDoc}
66
     */
67 10
    public function configure(object $object, DefinitionInterface $definition): object
68
    {
69 10
        $this->applyProperties($object, $definition->getProperties());
70 10
        $this->applyCalls($object, $definition->getCalls());
71 10
        return $object;
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77 1
    public function invoke(callable $callable, array $arguments = [])
78
    {
79 1
        return $this->call($callable, $arguments);
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85 2
    public function inflect(object $object, InflectionInterface $inflection): object
86
    {
87 2
        $this->applyProperties($object, $inflection->getProperties());
88 2
        $this->applyCalls($object, $inflection->getCalls());
89 2
        return $object;
90
    }
91
92
    /**
93
     * @param object $object
94
     * @param PropertyInterface[] $properties
95
     */
96 12
    protected function applyProperties(object $object, array $properties): void
97
    {
98 12
        foreach ($properties as $property) {
99 2
            $object->{$property->getName()} = $property->getValue();
100
        }
101 12
    }
102
103
    /**
104
     * @param object $object
105
     * @param CallInterface[] $calls
106
     * @throws NotFoundException
107
     */
108 12
    protected function applyCalls(object $object, array $calls): void
109
    {
110 12
        foreach ($calls as $call) {
111 4
            $this->call([$object, $call->getMethod()], $call->getArguments());
112
        }
113 12
    }
114
115
    /**
116
     * @param callable $callable
117
     * @param array $arguments
118
     * @return mixed
119
     * @throws NotFoundException
120
     */
121 5
    protected function call(callable $callable, array $arguments = [])
122
    {
123 5
        $dependencies = [];
124 5
        if ($this->analyzeArguments) {
125 5
            $dependencies = $this->dependenciesResolver->resolveCallableDependencies($callable);
126
        }
127 5
        $parameters = $this->buildParameters($arguments);
128 5
        $args = $this->argumentsBuilder->buildArguments($dependencies, $parameters);
129 5
        return call_user_func_array($callable, $args);
130
    }
131
132
    /**
133
     * Make object without factory
134
     *
135
     * @param DefinitionInterface $definition
136
     * @return mixed
137
     * @throws NotFoundException
138
     */
139 27
    protected function makeObjectSelf(DefinitionInterface $definition)
140
    {
141 27
        $dependencies = [];
142
143 27
        $className = $definition->getClass();
144 27
        if ($this->analyzeArguments) {
145 26
            $dependencies = $this->dependenciesResolver->resolveConstructorDependencies(
146 26
                $className,
147 26
                $definition->getConstructMethod()
148
            );
149
        }
150 27
        $parameters = $this->buildParameters($definition->getArguments());
151 27
        $arguments = $this->argumentsBuilder->buildArguments($dependencies, $parameters);
152
153 24
        return $this->constructObject($definition->getClass(), $arguments, $definition->getConstructMethod());
154
    }
155
156
    /**
157
     * Make object with factory
158
     *
159
     * @param DefinitionInterface $definition
160
     * @return mixed
161
     * @throws InvalidConfigurationException
162
     * @throws InvalidFactoryException
163
     * @throws NotFoundException
164
     */
165 12
    protected function makeObjectWithFactory(DefinitionInterface $definition)
166
    {
167 12
        $factory = $definition->getFactory();
168
169 12
        if (!is_callable($factory) || (is_array($factory))) {
170 9
            if (!$this->container) {
171 1
                throw new InvalidConfigurationException('Please, provide container for usage non-callable factories');
172
            }
173 8
            $factory = $this->buildFactoryFromNonCallable($definition);
174
        }
175
176 9
        $dependencies = [];
177 9
        if ($this->analyzeArguments) {
178 9
            $dependencies = $this->dependenciesResolver->resolveCallableDependencies($factory);
179
        }
180 9
        $parameters = $this->buildParameters($definition->getArguments());
181 9
        $arguments = $this->argumentsBuilder->buildArguments($dependencies, $parameters);
182 9
        return call_user_func_array($factory, $arguments);
183
    }
184
185
    /**
186
     * Build callable factory from non-callable
187
     *
188
     * @param DefinitionInterface $definition
189
     * @return callable
190
     * @throws InvalidFactoryException
191
     */
192 8
    protected function buildFactoryFromNonCallable(DefinitionInterface $definition): callable
193
    {
194 8
        $factory = $definition->getFactory();
195 8
        if (is_string($factory)) {
196 5
            return $this->resolveStringFactory($definition, $factory);
197
        }
198 3
        if (is_array($factory)) {
199 2
            return $this->resolveArrayFactory($definition, $factory);
200
        }
201 1
        throw new InvalidFactoryException('Incorrect factory provided, available string and array factories');
202
    }
203
204
    /**
205
     * @param DefinitionInterface $definition
206
     * @param string $factory
207
     * @return callable
208
     * @throws InvalidFactoryException
209
     */
210 5
    protected function resolveStringFactory(DefinitionInterface $definition, string $factory): callable
211
    {
212 5
        $id = $this->fetchDependencyId($factory);
213 5
        $method = $definition->getConstructMethod() ?: '__invoke';
214 5
        return $this->resolveFactoryFromContainer($id, $method);
215
    }
216
217
    /**
218
     * @param DefinitionInterface $definition
219
     * @param array $factory
220
     * @return callable
221
     * @throws InvalidFactoryException
222
     */
223 2
    protected function resolveArrayFactory(DefinitionInterface $definition, array $factory): callable
1 ignored issue
show
Unused Code introduced by
The parameter $definition is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
224
    {
225 2
        if (!isset($factory[0], $factory[1])) {
226
            throw new InvalidFactoryException('Array factory must contain 2 elements');
227
        }
228 2
        list($id, $method) = $factory;
229 2
        if (is_object($id)) {
230
            return $factory;
231
        }
232 2
        $id = $this->fetchDependencyId($id);
233 2
        return $this->resolveFactoryFromContainer($id, $method);
234
    }
235
236
    /**
237
     * Fetch factory from container
238
     *
239
     * @param $id
240
     * @param $method
241
     * @return callable
242
     * @throws InvalidFactoryException
243
     */
244 7
    protected function resolveFactoryFromContainer($id, $method): callable
245
    {
246 7
        if ($this->container->has($id)) {
247 6
            $factoryResolved = $this->container->get($id);
248 6
            return [$factoryResolved, $method];
249
        }
250 1
        throw new InvalidFactoryException('Incorrect factory provided');
251
    }
252
253
    /**
254
     * Create object with provided arguments and optional construct method
255
     *
256
     * @param string $className
257
     * @param array $arguments
258
     * @param string|null $constructMethod
259
     * @return mixed
260
     */
261 24
    protected function constructObject(string $className, array $arguments, ?string $constructMethod = null)
262
    {
263 24
        if ($constructMethod !== null) {
264 3
            $obj = $className::$constructMethod(...$arguments);
265
        } else {
266 21
            $obj = new $className(...$arguments);
267
        }
268 24
        return $obj;
269
    }
270
271
    /**
272
     * Build function attributes for type-value representation
273
     *
274
     * @param array $attributes
275
     * @return Parameter[]
276
     */
277 41
    protected function buildParameters($attributes = []): array
278
    {
279 41
        $parameters = [];
280 41
        foreach ($attributes as $key => $attribute) {
281 18
            $parameters[$key] = $this->buildParameter($attribute);
282
        }
283 41
        return $parameters;
284
    }
285
286
    /**
287
     * Create parameter from provided argument
288
     *
289
     * @param $value
290
     * @return ParameterInterface
291
     */
292 18
    protected function buildParameter($value): ParameterInterface
293
    {
294 18
        $type = ParameterInterface::TYPE_VALUE;
295 18
        if (\is_string($value) && 0 === strpos($value, '@')) {
296 6
            $type = ParameterInterface::TYPE_REFERENCE_REQUIRED;
297 6
            if (0 === strpos($value, '@?')) {
298 2
                $value = substr($value, 2);
299 2
                $type = ParameterInterface::TYPE_REFERENCE_OPTIONAL;
300 4
            } elseif (0 === strpos($value, '@@')) {
301 1
                $value = substr($value, 1);
302 1
                $type = DependencyInterface::TYPE_VALUE;
303
            } else {
304 3
                $value = substr($value, 1);
305
            }
306
        }
307 18
        return new Parameter($type, $value);
308
    }
309
310
    /**
311
     * Try fetch dependency name from string value
312
     *
313
     * @param string $value
314
     * @return string|null
315
     */
316 7
    protected function fetchDependencyId(string $value): ?string
317
    {
318 7
        if (0 === strpos($value, '@')) {
319 3
            return substr($value, 1);
320
        }
321 4
        return $value;
322
    }
323
}
324