Completed
Push — master ( 64cdca...103685 )
by Anton
01:21
created

Builder::inflect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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 $autoWire = true;
32
33 61
    public function __construct(bool $autoWire = true, ?DependenciesResolverInterface $dependenciesResolver = null)
34
    {
35 61
        $this->autoWire = $autoWire;
36 61
        $this->dependenciesResolver = $dependenciesResolver ?: new DependenciesResolver();
37 61
        $this->argumentsBuilder = new ArgumentsBuilder();
38 61
    }
39
40
    /**
41
     * {@inheritDoc}
42
     */
43 48
    public function setContainer(ContainerInterface $container): void
44
    {
45 48
        $this->container = $container;
46 48
        $this->argumentsBuilder->setContainer($container);
47 48
    }
48
49
    /**
50
     * {@inheritDoc}
51
     */
52 39
    public function construct(DefinitionInterface $definition): object
53
    {
54 39
        if ($definition->getFactory()) {
55 12
            $object = $this->makeObjectWithFactory($definition);
56
        } else {
57 27
            $object = $this->makeObjectSelf($definition);
58
        }
59 33
        return $object;
60
    }
61
62
    /**
63
     * {@inheritDoc}
64
     */
65 10
    public function configure(object $object, DefinitionInterface $definition): object
66
    {
67 10
        $this->applyProperties($object, $definition->getProperties());
68 10
        $this->applyCalls($object, $definition->getCalls());
69 10
        return $object;
70
    }
71
72
    /**
73
     * {@inheritDoc}
74
     */
75 1
    public function invoke(callable $callable, array $arguments = [])
76
    {
77 1
        return $this->call($callable, $arguments);
78
    }
79
80
    /**
81
     * {@inheritDoc}
82
     */
83 2
    public function inflect(object $object, InflectionInterface $inflection): object
84
    {
85 2
        $this->applyProperties($object, $inflection->getProperties());
86 2
        $this->applyCalls($object, $inflection->getCalls());
87 2
        return $object;
88
    }
89
90
    /**
91
     * @param object $object
92
     * @param PropertyInterface[] $properties
93
     */
94 12
    protected function applyProperties(object $object, array $properties): void
95
    {
96 12
        foreach ($properties as $property) {
97 2
            $object->{$property->getName()} = $property->getValue();
98
        }
99 12
    }
100
101
    /**
102
     * @param object $object
103
     * @param CallInterface[] $calls
104
     * @throws NotFoundException
105
     */
106 12
    protected function applyCalls(object $object, array $calls): void
107
    {
108 12
        foreach ($calls as $call) {
109 4
            $this->call([$object, $call->getMethod()], $call->getArguments());
110
        }
111 12
    }
112
113
    /**
114
     * @param callable $callable
115
     * @param array $arguments
116
     * @return mixed
117
     * @throws NotFoundException
118
     */
119 5
    protected function call(callable $callable, array $arguments = [])
120
    {
121 5
        $dependencies = [];
122 5
        if ($this->autoWire) {
123 5
            $dependencies = $this->dependenciesResolver->resolveCallableDependencies($callable);
124
        }
125 5
        $parameters = $this->buildParameters($arguments);
126 5
        $args = $this->argumentsBuilder->buildArguments($dependencies, $parameters);
127 5
        return call_user_func_array($callable, $args);
128
    }
129
130
    /**
131
     * Make object without factory
132
     *
133
     * @param DefinitionInterface $definition
134
     * @return mixed
135
     * @throws NotFoundException
136
     */
137 27
    protected function makeObjectSelf(DefinitionInterface $definition)
138
    {
139 27
        $dependencies = [];
140
141 27
        $className = $definition->getClass();
142 27
        if ($this->autoWire) {
143 26
            $dependencies = $this->dependenciesResolver->resolveConstructorDependencies(
144 26
                $className,
145 26
                $definition->getConstructMethod()
146
            );
147
        }
148 27
        $parameters = $this->buildParameters($definition->getArguments());
149 27
        $arguments = $this->argumentsBuilder->buildArguments($dependencies, $parameters);
150
151 24
        return $this->constructObject($definition->getClass(), $arguments, $definition->getConstructMethod());
152
    }
153
154
    /**
155
     * Make object with factory
156
     *
157
     * @param DefinitionInterface $definition
158
     * @return mixed
159
     * @throws InvalidConfigurationException
160
     * @throws InvalidFactoryException
161
     * @throws NotFoundException
162
     */
163 12
    protected function makeObjectWithFactory(DefinitionInterface $definition)
164
    {
165 12
        $factory = $definition->getFactory();
166
167 12
        if (!is_callable($factory) || is_array($factory)) {
168 9
            if (!$this->container) {
169 1
                throw new InvalidConfigurationException('Please, provide container for usage non-callable factories');
170
            }
171 8
            $factory = $this->buildFactoryFromNonCallable($definition);
172
        }
173
174 9
        $dependencies = [];
175 9
        if ($this->autoWire) {
176 9
            $dependencies = $this->dependenciesResolver->resolveCallableDependencies($factory);
177
        }
178 9
        $parameters = $this->buildParameters($definition->getArguments());
179 9
        $arguments = $this->argumentsBuilder->buildArguments($dependencies, $parameters);
180 9
        return call_user_func_array($factory, $arguments);
181
    }
182
183
    /**
184
     * Build callable factory from non-callable
185
     *
186
     * @param DefinitionInterface $definition
187
     * @return callable
188
     * @throws InvalidFactoryException
189
     */
190 8
    protected function buildFactoryFromNonCallable(DefinitionInterface $definition): callable
191
    {
192 8
        $factory = $definition->getFactory();
193 8
        if (is_string($factory)) {
194 5
            $factoryId = $this->fetchDependencyId($factory);
195 5
            $factoryMethod = $definition->getConstructMethod() ?: '__invoke';
196 3
        } elseif (is_array($factory)) {
197 2
            $factoryId = $this->fetchDependencyId($factory[0]);
198 2
            $factoryMethod = $factory[1];
199
        } else {
200 1
            throw new InvalidFactoryException('Incorrect factory provided, available string and array factories');
201
        }
202 7
        if ($this->container->has($factoryId)) {
203 6
            $factoryResolved = $this->container->get($factoryId);
204 6
            return [$factoryResolved, $factoryMethod];
205
        }
206 1
        throw new InvalidFactoryException('Incorrect factory provided');
207
    }
208
209
    /**
210
     * Create object with provided arguments and optional construct method
211
     *
212
     * @param string $className
213
     * @param array $arguments
214
     * @param string|null $constructMethod
215
     * @return mixed
216
     */
217 24
    protected function constructObject(string $className, array $arguments, ?string $constructMethod = null)
218
    {
219 24
        if ($constructMethod !== null) {
220 3
            $obj = $className::$constructMethod(...$arguments);
221
        } else {
222 21
            $obj = new $className(...$arguments);
223
        }
224 24
        return $obj;
225
    }
226
227
    /**
228
     * Build function attributes for type-value representation
229
     *
230
     * @param array $attributes
231
     * @return Parameter[]
232
     */
233 41
    protected function buildParameters($attributes = []): array
234
    {
235 41
        $parameters = [];
236 41
        foreach ($attributes as $key => $attribute) {
237 18
            $parameters[$key] = $this->buildParameter($attribute);
238
        }
239 41
        return $parameters;
240
    }
241
242
    /**
243
     * Create parameter from provided argument
244
     *
245
     * @param $value
246
     * @return ParameterInterface
247
     */
248 18
    protected function buildParameter($value): ParameterInterface
249
    {
250 18
        $type = ParameterInterface::TYPE_VALUE;
251 18
        if (\is_string($value) && 0 === strpos($value, '@')) {
252 6
            $type = ParameterInterface::TYPE_REFERENCE_REQUIRED;
253 6
            if (0 === strpos($value, '@?')) {
254 2
                $value = substr($value, 2);
255 2
                $type = ParameterInterface::TYPE_REFERENCE_OPTIONAL;
256 4
            } elseif (0 === strpos($value, '@@')) {
257 1
                $value = substr($value, 1);
258 1
                $type = DependencyInterface::TYPE_VALUE;
259
            } else {
260 3
                $value = substr($value, 1);
261
            }
262
        }
263 18
        return new Parameter($type, $value);
264
    }
265
266
    /**
267
     * Try fetch dependency name from string value
268
     *
269
     * @param string $value
270
     * @return string|null
271
     */
272 7
    protected function fetchDependencyId(string $value): ?string
273
    {
274 7
        if (0 === strpos($value, '@')) {
275 3
            return substr($value, 1);
276
        }
277 4
        return $value;
278
    }
279
}
280