Completed
Push — master ( 1cc7f6...ad43be )
by Ailis
04:10 queued 01:39
created

ImmutableValue::cloneArray()   A

Complexity

Conditions 1
Paths 1

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 1
nc 1
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 = strtolower($name{4}) . substr($name, 5);
93
            if (array_key_exists($memberName, $this->data)) {
94
                $value = self::cloneValue($arguments[0]);
95
                $result = clone $this;
96
                $result->data[$memberName] = $value;
97
                return $result;
98
            }
99
        }
100
        $json = json_encode($arguments);
101
        throw new RuntimeException(
102
            'Cannot evaluate (' . get_class($this) . ')->' . $name . '(' . substr($json, 1, -1) . ')'
103
        );
104
    }
105
106
    public function __clone()
107
    {
108
        $this->data = self::cloneValue($this->data);
109
    }
110
111
    /**
112
     * @param string $name
113
     *
114
     * @return mixed
115
     * @throws RuntimeException
116
     */
117
    public function __get(string $name)
118
    {
119
        if (array_key_exists($name, $this->data)) {
120
            return $this->data[$name];
121
        }
122
        throw new RuntimeException('Cannot access (' . get_class($this) . ')->$' . $name);
123
    }
124
125
    /**
126
     * Checks whether this instance is equal to the given value.
127
     *
128
     * @param $other
129
     *
130
     * @return bool
131
     */
132
    public function equals($other): bool
133
    {
134
        return $other instanceof static && self::isEqual(get_object_vars($this), get_object_vars($other));
135
    }
136
137
    /**
138
     * @param string $name
139
     * @param mixed $value
140
     *
141
     * @throws InvalidArgumentException
142
     */
143
    protected function init(string $name, $value): void
144
    {
145
        if (array_key_exists($name, $this->data)) {
146
            throw new InvalidArgumentException(
147
                sprintf('Property %s of %s cannot be re-initialized', $name, get_class($this))
148
            );
149
        }
150
        $this->data[$name] = self::cloneValue($value);
151
    }
152
}
153
154
__halt_compiler();
155
/**
156
 * @property-read int $red
157
 * @property-read int $green
158
 * @property-read int $blue
159
 * @method self withRed(int $v)
160
 * @method self withGreen(int $v)
161
 * @method self withBlue(int $v)
162
 */
163
final class Color extends ImmutableValue
164
{
165
    public function __construct(int $red, int $green, int $blue)
166
    {
167
        $this->init('red', $red);
168
        $this->init('green', $green);
169
        $this->init('blue', $blue);
170
    }
171
}
172