ScopedHydrator   A
last analyzed

Complexity

Total Complexity 10

Size/Duplication

Total Lines 77
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 10
eloc 27
c 3
b 0
f 0
dl 0
loc 77
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A writeTo() 0 8 3
A write() 0 20 3
A __construct() 0 7 2
A prefixedWith() 0 3 1
A default() 0 3 1
1
<?php declare(strict_types=1);
2
3
namespace Stratadox\Hydrator;
4
5
use InvalidArgumentException;
6
use ReflectionClass;
7
use ReflectionException;
8
use ReflectionObject;
9
use function sprintf;
10
use function strlen;
11
use function strpos;
12
use function substr;
13
use Throwable;
14
15
/**
16
 * Hydrator properties in a specific scope by deconstructing the input.
17
 *
18
 * Useful in the specific case where a child class has a private property, while
19
 * its parent(s) also have a private property by that very same name.
20
 * In those edge cases, the reflective hydrator cannot correctly determine the
21
 * property scope, nor would client code be able to pass both properties in the
22
 * same map.
23
 * This hydrator avoids that problem by requiring an explicit scope in the form
24
 * of one or more subsequent prefixes, for example:
25
 * `{"property": "foo", "parent.property": "bar"}`
26
 *
27
 * @package Stratadox\Hydrate
28
 * @author  Stratadox
29
 */
30
final class ScopedHydrator implements Hydrator
31
{
32
    /** @var string */
33
    private $prefix;
34
    /** @var int */
35
    private $prefixLength;
36
37
    private function __construct(string $prefix)
38
    {
39
        $this->prefix = $prefix;
40
        $this->prefixLength = strlen($prefix);
41
        if (!$this->prefixLength) {
42
            throw new InvalidArgumentException(
43
                'The prefix for the scoped hydrator cannot be empty.'
44
            );
45
        }
46
    }
47
48
    /**
49
     * Produce a scoped hydrator.
50
     *
51
     * @return Hydrator A hydrator that uses prefixes to determine the scopes.
52
     */
53
    public static function default(): Hydrator
54
    {
55
        return new self('parent.');
56
    }
57
58
    /**
59
     * Produce a scoped hydrator with a custom prefix.
60
     *
61
     * @param string $prefix  The prefix to determine the parental scope.
62
     * @return ScopedHydrator A hydrator that uses the custom prefix.
63
     */
64
    public static function prefixedWith(string $prefix): self
65
    {
66
        return new self($prefix);
67
    }
68
69
    /** @inheritdoc */
70
    public function writeTo(object $target, array $data): void
71
    {
72
        $object = new ReflectionObject($target);
73
        foreach ($data as $name => $value) {
74
            try {
75
                $this->write($object, $target, $name, $value);
76
            } catch (Throwable $exception) {
77
                throw HydrationFailed::encountered($exception, $target);
78
            }
79
        }
80
    }
81
82
    /**
83
     * @param mixed $value
84
     * @var ReflectionClass|bool $class
85
     * @throws ReflectionException
86
     */
87
    private function write(
88
        ReflectionClass $class,
89
        object $target,
90
        string $propertyName,
91
        $value
92
    ): void {
93
        $name = $propertyName;
94
        while (strpos($name, $this->prefix) === 0) {
95
            $name = substr($name, $this->prefixLength);
96
            $class = $class->getParentClass();
97
            if (!$class) {
98
                throw new InvalidArgumentException(sprintf(
99
                    'It has no %s.',
100
                    $propertyName
101
                ));
102
            }
103
        }
104
        $property = $class->getProperty($name);
105
        $property->setAccessible(true);
106
        $property->setValue($target, $value);
107
    }
108
}
109