Passed
Push — PSR-11-2 ( dfb1a3...d7a3e8 )
by Nikolaos
04:33
created

Resolver::processBlueprint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 16
ccs 10
cts 10
cp 1
rs 9.9666
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by AuraPHP
12
 *
13
 * @link    https://github.com/auraphp/Aura.Di
14
 * @license https://github.com/auraphp/Aura.Di/blob/4.x/LICENSE
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phalcon\Container\Resolver;
20
21
use Phalcon\Container\Exception;
22
use Phalcon\Container\Exception\NoSuchProperty;
23
use ReflectionException;
24
use ReflectionParameter;
25
26
use function array_merge;
27
28
/**
29
 * Resolves class creation specifics based on constructor params and setter
30
 * definitions, unified across class defaults, inheritance hierarchies, and
31
 * configuration.
32
 *
33
 * @property ValueObject    $mutations
34
 * @property ValueObject    $parameters
35
 * @property Reflector      $reflector
36
 * @property ValueObject    $setters
37
 * @property ValueObject    $values
38
 * @property ValueObject    $unified
39
 */
40
class Resolver
41
{
42
    /**
43
     * Setter definitions in the form of `$mutations[$class][] = $value`.
44
     *
45
     * @var ValueObject
46
     */
47
    private $mutations;
48
49
    /**
50
     * Constructor params in the form `$params[$class][$name] = $value`.
51
     *
52
     * @var ValueObject
53
     */
54
    private $parameters;
55
56
    /**
57
     * A Reflector.
58
     *
59
     * @var Reflector
60
     */
61
    private $reflector;
62
63
    /**
64
     * Setter definitions in the form of `$setters[$class][$method] = $value`.
65
     *
66
     * @var ValueObject
67
     */
68
    private $setters;
69
70
    /**
71
     * Constructor params and setter definitions, unified across class
72
     * defaults, inheritance hierarchies, and configuration.
73
     *
74
     * @var ValueObject
75
     */
76
    private $unified;
77
78
    /**
79
     * Arbitrary values in the form of `$values[$key] = $value`.
80
     *
81
     * @var ValueObject
82
     */
83
    private $values;
84
85
    /**
86
     *
87
     * Constructor.
88
     *
89
     * @param Reflector $reflector A collection point for Reflection data.
90
     *
91
     */
92 78
    public function __construct(Reflector $reflector)
93
    {
94 78
        $this->reflector  = $reflector;
95 78
        $this->mutations  = new ValueObject();
96 78
        $this->parameters = new ValueObject();
97 78
        $this->setters    = new ValueObject();
98 78
        $this->unified    = new ValueObject();
99 78
        $this->values     = new ValueObject();
100 78
    }
101
102
    /**
103
     * Returns the unified constructor params and setters for a class.
104
     *
105
     * @param string $class The class name to return values for.
106
     *
107
     * @return Blueprint A blueprint how to construct an object
108
     * @throws NoSuchProperty
109
     * @throws ReflectionException
110
     */
111 44
    public function getUnified(string $class): Blueprint
112
    {
113
        // have values already been unified for this class?
114 44
        if ($this->unified->has($class)) {
115 2
            return $this->unified->get($class);
116
        }
117
118
        // fetch the values for parents so we can inherit them
119 44
        $parent = get_parent_class($class);
120 44
        if ($parent) {
121 9
            $spec = $this->getUnified($parent);
122
        } else {
123 44
            $spec = new Blueprint($class);
124
        }
125
126
        // stores the unified params and setters
127 44
        $this->unified->set(
128 44
            $class,
129 44
            new Blueprint(
130 44
                $class,
131 44
                $this->getUnifiedParameters($class, $spec->getParameters()),
132 43
                $this->getUnifiedSetters($class, $spec->getSetters()),
133 43
                $this->getUnifiedMutations($class, $spec->getMutations())
134
            )
135
        );
136
137
        // done, return the unified values
138 43
        return $this->unified->get($class);
139
    }
140
141
    /**
142
     * @return ValueObject
143
     */
144 5
    public function mutations(): ValueObject
145
    {
146 5
        return $this->mutations;
147
    }
148
149
    /**
150
     * @return ValueObject
151
     */
152 11
    public function parameters(): ValueObject
153
    {
154 11
        return $this->parameters;
155
    }
156
157
    /**
158
     * Creates and returns a new instance of a class using reflection and
159
     * the configuration parameters, optionally with overrides, invoking Lazy
160
     * values along the way.
161
     *
162
     * @param Blueprint $blueprint The blueprint to be resolved containing
163
     *                             its overrides for this specific case.
164
     * @param array     $contextualBlueprints
165
     *
166
     * @return object
167
     * @throws NoSuchProperty
168
     * @throws ReflectionException
169
     */
170 31
    public function resolve(Blueprint $blueprint, array $contextualBlueprints = []): object
171
    {
172 31
        if ($contextualBlueprints === []) {
173 31
            return call_user_func(
174 31
                $this->expandParameters(
175
                    $this
176 31
                        ->getUnified($blueprint->getClassName())
177 30
                        ->merge($blueprint)
178
                ),
179 30
                $this->reflector->getClass($blueprint->getClassName())
180
            );
181
        }
182
183 1
        $remember = new self($this->reflector);
184
185 1
        foreach ($contextualBlueprints as $contextualBlueprint) {
186 1
            $className = $contextualBlueprint->getClassName();
187 1
            $this->processBlueprint($className, $remember, $this);
188
189 1
            $this->parameters->merge($className, $contextualBlueprint->getParameters());
190 1
            $this->setters->merge($className, $contextualBlueprint->getSetters());
191 1
            $this->mutations->merge($className, $contextualBlueprint->getMutations());
192
193 1
            $this->unified->remove($className);
194
        }
195
196 1
        $resolved = call_user_func(
197 1
            $this->expandParameters(
198 1
                $this->getUnified($blueprint->getClassName())->merge($blueprint)
199
            ),
200 1
            $this->reflector->getClass($blueprint->getClassName())
201
        );
202
203 1
        foreach ($contextualBlueprints as $contextualBlueprint) {
204 1
            $className = $contextualBlueprint->getClassName();
205 1
            $this->processBlueprint($className, $this, $remember);
206
207 1
            if ($remember->unified->has($className)) {
208
                $this->unified->set($className, $remember->unified->get($className));
209
            } else {
210 1
                $this->unified->remove($className);
211
            }
212
        }
213
214 1
        return $resolved;
215
    }
216
217
    /**
218
     * @return ValueObject
219
     */
220 9
    public function setters(): ValueObject
221
    {
222 9
        return $this->setters;
223
    }
224
225
    /**
226
     * @return ValueObject
227
     */
228 1
    public function unified(): ValueObject
229
    {
230 1
        return $this->unified;
231
    }
232
233
    /**
234
     * @return ValueObject
235
     */
236 5
    public function values(): ValueObject
237
    {
238 5
        return $this->values;
239
    }
240
241
    /**
242
     * Returns the unified mutations for a class.
243
     *
244
     * Class-specific mutations are executed last before trait-based mutations
245
     * and before interface-based mutations.
246
     *
247
     * @param string $class  The class name to return values for.
248
     * @param array  $parent The parent unified setters.
249
     *
250
     * @return array The unified mutations.
251
     * @throws NoSuchProperty
252
     */
253 43
    protected function getUnifiedMutations(string $class, array $parent): array
254
    {
255 43
        $unified = $parent;
256
257
        // look for interface mutations
258 43
        $interfaces = class_implements($class);
259 43
        foreach ($interfaces as $interface) {
260 3
            if ($this->mutations->has($interface)) {
261
                $unified = $this->mutations->merge($interface, $unified);
262
            }
263
        }
264
265
        // look for trait mutations
266 43
        $traits = $this->reflector->getTraits($class);
267 43
        foreach ($traits as $trait) {
268 5
            if ($this->mutations->has($trait)) {
269
                $unified = $this->mutations->merge($trait, $unified);
270
            }
271
        }
272
273
        // look for class mutations
274 43
        if ($this->mutations->has($class)) {
275 4
            $unified = array_merge(
276 4
                $unified,
277 4
                $this->mutations->get($class)
278
            );
279
        }
280
281 43
        return $unified;
282
    }
283
284
    /**
285
     * Returns a unified param.
286
     *
287
     * @param ReflectionParameter $rparam A parameter reflection.
288
     * @param string              $class  The class name to return values for.
289
     * @param array               $parent The parent unified params.
290
     *
291
     * @return mixed|DefaultValueParameter|UnresolvedParameter
292
     * @throws NoSuchProperty
293
     * @throws ReflectionException
294
     */
295 31
    protected function getUnifiedParameter(
296
        ReflectionParameter $rparam,
297
        string $class,
298
        array $parent
299
    ) {
300 31
        $name     = $rparam->getName();
301 31
        $position = $rparam->getPosition();
302
303
        // is there a positional value explicitly from the current class?
304 31
        $explicitPosition = $this->parameters->has($class)
305 31
            && array_key_exists($position, $this->parameters->get($class))
306 31
            && !$this->parameters->get($class)[$position] instanceof UnresolvedParameter;
307
308 31
        if ($explicitPosition) {
309 1
            return $this->parameters->get($class)[$position];
310
        }
311
312
        // is there a named value explicitly from the current class?
313 31
        $explicitNamed = $this->parameters->has($class)
314 31
            && array_key_exists($name, $this->parameters->get($class))
315 31
            && !$this->parameters->get($class)[$name] instanceof UnresolvedParameter;
316
317 31
        if ($explicitNamed) {
318 8
            return $this->parameters->get($class)[$name];
319
        }
320
321
        // is there a named value implicitly inherited from the parent class?
322
        // (there cannot be a positional parent. this is because the unified
323
        // values are stored by name, not position.)
324 24
        $implicitNamed = array_key_exists($name, $parent)
325 24
            && !$parent[$name] instanceof UnresolvedParameter
326 24
            && !$parent[$name] instanceof DefaultValueParameter;
327
328 24
        if ($implicitNamed) {
329 2
            return $parent[$name];
330
        }
331
332
        // is a default value available for the current class?
333 23
        if ($rparam->isDefaultValueAvailable()) {
334 17
            return new DefaultValueParameter($name, $rparam->getDefaultValue());
335
        }
336
337
        // is a default value available for the parent class?
338 13
        $parentDefault = array_key_exists($name, $parent)
339 13
            && $parent[$name] instanceof DefaultValueParameter;
340 13
        if ($parentDefault) {
341 6
            return $parent[$name];
342
        }
343
344
        // param is missing
345 8
        return new UnresolvedParameter($name);
346
    }
347
348
    /**
349
     * Returns the unified constructor params for a class.
350
     *
351
     * @param string $class  The class name to return values for.
352
     * @param array  $parent The parent unified params.
353
     *
354
     * @return array The unified params.
355
     * @throws ReflectionException
356
     */
357 44
    protected function getUnifiedParameters(string $class, array $parent): array
358
    {
359
        // reflect on what params to pass, in which order
360 44
        $unified = [];
361 44
        $rparams = $this->reflector->getParameters($class);
362 43
        foreach ($rparams as $rparam) {
363 31
            $unified[$rparam->name] = $this->getUnifiedParameter(
364 31
                $rparam,
365
                $class,
366
                $parent
367
            );
368
        }
369
370 43
        return $unified;
371
    }
372
373
    /**
374
     * Returns the unified setters for a class.
375
     *
376
     * Class-specific setters take precedence over trait-based setters, which
377
     * take precedence over interface-based setters.
378
     *
379
     * @param string $class  The class name to return values for.
380
     * @param array  $parent The parent unified setters.
381
     *
382
     * @return array The unified setters.
383
     * @throws NoSuchProperty
384
     */
385 43
    protected function getUnifiedSetters(string $class, array $parent): array
386
    {
387 43
        $unified = $parent;
388
        // look for interface setters
389 43
        $interfaces = class_implements($class);
390 43
        foreach ($interfaces as $interface) {
391 3
            if ($this->setters->has($interface)) {
392
                $unified = $this->setters->merge($interface, $unified);
393
            }
394
        }
395
396
        // look for trait setters
397 43
        $traits = $this->reflector->getTraits($class);
398 43
        foreach ($traits as $trait) {
399 5
            if ($this->setters->has($trait)) {
400 5
                $unified = $this->setters->merge($trait, $unified);
401
            }
402
        }
403
404
        // look for class setters
405 43
        if ($this->setters->has($class)) {
406 4
            $unified = array_merge(
407 4
                $unified,
408 4
                $this->setters->get($class)
409
            );
410
        }
411
412 43
        return $unified;
413
    }
414
415
    /**
416
     * Expands variadic parameters onto the end of a constructor parameters
417
     * array.
418
     *
419
     * @param Blueprint $blueprint The blueprint to expand parameters for.
420
     *
421
     * @return Blueprint The blueprint with expanded constructor parameters.
422
     * @throws ReflectionException
423
     */
424 30
    protected function expandParameters(Blueprint $blueprint): Blueprint
425
    {
426 30
        $class  = $blueprint->getClassName();
427 30
        $params = $blueprint->getParameters();
428
429 30
        $variadicParams = [];
430 30
        foreach ($this->reflector->getParameters($class) as $reflectParam) {
431 23
            $paramName = $reflectParam->getName();
432 23
            if ($reflectParam->isVariadic() && is_array($params[$paramName])) {
433 2
                $variadicParams = array_merge($variadicParams, $params[$paramName]);
434 2
                unset($params[$paramName]);
435 2
                break; // There can only be one
436
            }
437
438 23
            if ($params[$paramName] instanceof DefaultValueParameter) {
439 6
                $params[$paramName] = $params[$paramName]->getValue();
440
            }
441
        }
442
443 30
        return $blueprint->replaceParameters(
444 30
            array_merge($params, array_values($variadicParams))
445
        );
446
    }
447
448
    /**
449
     * @param string   $className
450
     * @param Resolver $source
451
     * @param Resolver $target
452
     */
453 1
    private function processBlueprint(
454
        string $className,
455
        Resolver $source,
456
        Resolver $target
457
    ): void {
458 1
        $source->parameters->set(
459 1
            $className,
460 1
            $target->parameters->getWithDefault($className, [])
461
        );
462 1
        $source->setters->set(
463 1
            $className,
464 1
            $target->setters->getWithDefault($className, [])
465
        );
466 1
        $source->mutations->set(
467 1
            $className,
468 1
            $target->mutations->getWithDefault($className, [])
469
        );
470 1
    }
471
}
472