ImmutableValue::cloneValue()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php declare(strict_types=1);
2
namespace Kepawni\Twilted\Basic;
3
4
use InvalidArgumentException;
5
use Kepawni\Twilted\Equatable;
6
use RuntimeException;
7
8
/**
9
 * Subclasses (which should be final) are immutable and their properties are externally available through magic getters
10
 * and internally through a protected init method. For each of the properties there is a public method “with...($value)”
11
 * that returns a deep copy of the instance which differs in that property being set to the new value. It is recommended
12
 * to use the documentation tags @​property-read and @​method to help IDEs providing intellisense capabilities anyway.
13
 *
14
 * CAUTION: The following example contains Zero-Width Space characters before / and after @ to prevent interpretation of
15
 * doctags and premature comment ending. For a more comfortable copy-and-paste experience this source code can be found
16
 * at the bottom of the file without those extra ZWS characters.
17
 *
18
 * Example:
19
 *
20
 *     /**
21
 *      * @​property-read int $red
22
 *      * @​property-read int $green
23
 *      * @​property-read int $blue
24
 *      * @​method self withRed(int $v)
25
 *      * @​method self withGreen(int $v)
26
 *      * @​method self withBlue(int $v)
27
 *      *​/
28
 *     final class Color extends ImmutableValue
29
 *     {
30
 *         public function __construct(int $red, int $green, int $blue)
31
 *         {
32
 *             $this->init('red', $red);
33
 *             $this->init('green', $green);
34
 *             $this->init('blue', $blue);
35
 *         }
36
 *     }
37
 */
38
abstract class ImmutableValue implements Equatable
39
{
40
    private $data = [];
41
42
    private static function areArraysAndEqual($a, $b): bool
43
    {
44
        return self::areArraysAndSameSized($a, $b) && count($a) === self::countEqualValuesInArrays($a, $b);
45
    }
46
47
    private static function areArraysAndSameSized($a, $b): bool
48
    {
49
        return is_array($a) && is_array($b) && count($a) === count($b);
50
    }
51
52
    private static function areInstancesAndEqual($a, $b): bool
53
    {
54
        return $a instanceof self && $a->equals($b);
55
    }
56
57
    private static function cloneArray($value): array
58
    {
59
        return array_map([self::class, 'cloneValue'], $value);
60
    }
61
62
    private static function cloneIfObject($value)
63
    {
64
        return is_object($value) ? clone $value : $value;
65
    }
66
67
    private static function cloneValue($value)
68
    {
69
        return is_array($value) ? self::cloneArray($value) : self::cloneIfObject($value);
70
    }
71
72
    private static function countEqualValuesInArrays($a, $b): int
73
    {
74
        return count(array_filter(array_map([self::class, 'isEqual'], $a, $b)));
75
    }
76
77
    private static function isEqual($a, $b): bool
78
    {
79
        return $a === $b || self::areArraysAndEqual($a, $b) || self::areInstancesAndEqual($a, $b);
80
    }
81
82
    /**
83
     * @param string $name
84
     * @param array $arguments
85
     *
86
     * @return ImmutableValue
87
     * @throws RuntimeException
88
     */
89
    public function __call(string $name, array $arguments)
90
    {
91
        if (strlen($name) > 4 && substr($name, 0, 4) === 'with' && count($arguments) === 1) {
92
            $memberName = array_key_exists(strtolower($name[4]) . substr($name, 5), $this->data)
93
                ? strtolower($name[4]) . substr($name, 5)
94
                : substr($name, 4);
95
            if (array_key_exists($memberName, $this->data)) {
96
                $value = self::cloneValue($arguments[0]);
97
                $result = clone $this;
98
                $result->data[$memberName] = $value;
99
                return $result;
100
            }
101
        }
102
        $json = json_encode($arguments);
103
        throw new RuntimeException(
104
            'Cannot evaluate (' . get_class($this) . ')->' . $name . '(' . substr($json, 1, -1) . ')'
105
        );
106
    }
107
108
    public function __clone()
109
    {
110
        $this->data = self::cloneValue($this->data);
111
    }
112
113
    /**
114
     * @param string $name
115
     *
116
     * @return mixed
117
     * @throws RuntimeException
118
     */
119
    public function __get(string $name)
120
    {
121
        if (array_key_exists($name, $this->data)) {
122
            return $this->data[$name];
123
        }
124
        throw new RuntimeException('Cannot access (' . get_class($this) . ')->$' . $name);
125
    }
126
127
    /**
128
     * Checks whether this instance is equal to the given value.
129
     *
130
     * @param $other
131
     *
132
     * @return bool
133
     */
134
    public function equals($other): bool
135
    {
136
        return $other instanceof static && self::isEqual(get_object_vars($this), get_object_vars($other));
137
    }
138
139
    /**
140
     * @param string $name
141
     * @param mixed $value
142
     *
143
     * @throws InvalidArgumentException
144
     */
145
    protected function init(string $name, $value): void
146
    {
147
        if (array_key_exists($name, $this->data)) {
148
            throw new InvalidArgumentException(
149
                sprintf('Property %s of %s cannot be re-initialized', $name, get_class($this))
150
            );
151
        }
152
        $this->data[$name] = self::cloneValue($value);
153
    }
154
}
155
156
__halt_compiler();
157
/**
158
 * @property-read int $red
159
 * @property-read int $green
160
 * @property-read int $blue
161
 * @method self withRed(int $v)
162
 * @method self withGreen(int $v)
163
 * @method self withBlue(int $v)
164
 */
165
final class Color extends ImmutableValue
166
{
167
    public function __construct(int $red, int $green, int $blue)
168
    {
169
        $this->init('red', $red);
170
        $this->init('green', $green);
171
        $this->init('blue', $blue);
172
    }
173
}
174