Passed
Push — master ( fbc474...2a96e4 )
by Jesse
05:27
created

ScopedHydrator::prefixedWith()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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