Completed
Push — master ( ad9b26...a26aca )
by Julián
03:06
created

ImmutabilityBehaviour   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 84
c 7
b 0
f 0
dl 0
loc 264
rs 10
wmc 28

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __unset() 0 3 1
A __call() 0 3 1
A __set() 0 3 1
A __invoke() 0 3 1
A checkImmutability() 0 8 1
A assertImmutabilitySingleCheck() 0 10 2
A assertImmutable() 0 15 2
A getClassPublicMethods() 0 12 2
A getFilteredImmutabilityCallStack() 0 13 5
A assertPropertyVisibility() 0 7 2
A getAllowedPublicMethods() 0 24 2
A assertImmutabilityCallConstraints() 0 14 5
A assertMethodVisibility() 0 11 3
1
<?php
2
3
/*
4
 * PHPGears immutability (https://github.com/phpgears/immutability).
5
 * Object immutability guard for PHP.
6
 *
7
 * @license MIT
8
 * @link https://github.com/phpgears/immutability
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Gears\Immutability;
15
16
use Gears\Immutability\Exception\ImmutabilityViolationException;
17
18
/**
19
 * Immutability check behaviour.
20
 */
21
trait ImmutabilityBehaviour
22
{
23
    /**
24
     * Class immutability checked map.
25
     *
26
     * @var bool[]
27
     */
28
    protected static $immutabilityCheckMap = [];
29
30
    /**
31
     * List of default allowed magic methods.
32
     *
33
     * @var string[]
34
     */
35
    protected static $allowedMagicMethods = [
36
        '__construct',
37
        '__destruct',
38
        '__get',
39
        '__isset',
40
        '__sleep',
41
        '__wakeup',
42
        '__serialize',
43
        '__unserialize',
44
        '__toString',
45
        '__set_state',
46
        '__clone',
47
        '__debugInfo',
48
    ];
49
50
    /**
51
     * Single constructor call check.
52
     *
53
     * @var bool
54
     */
55
    private $immutabilityAlreadyChecked = false;
56
57
    /**
58
     * Alias of assertImmutable.
59
     *
60
     * @deprecated use assertImmutable instead
61
     */
62
    final protected function checkImmutability(): void
63
    {
64
        @\trigger_error(
65
            'Calling the "checkImmutability()" method is deprecated. Use "assertImmutable()" method instead',
66
            \E_USER_DEPRECATED
67
        );
68
69
        $this->assertImmutable();
70
    }
71
72
    /**
73
     * Assert object immutability.
74
     *
75
     * @throws ImmutabilityViolationException
76
     */
77
    final protected function assertImmutable(): void
78
    {
79
        $this->assertImmutabilitySingleCheck();
80
81
        $class = static::class;
82
83
        if (isset(static::$immutabilityCheckMap[$class])) {
84
            return;
85
        }
86
87
        $this->assertImmutabilityCallConstraints();
88
        $this->assertPropertyVisibility();
89
        $this->assertMethodVisibility();
90
91
        static::$immutabilityCheckMap[$class] = true;
92
    }
93
94
    /**
95
     * Assert single immutability check.
96
     *
97
     * @throws ImmutabilityViolationException
98
     */
99
    private function assertImmutabilitySingleCheck(): void
100
    {
101
        if ($this->immutabilityAlreadyChecked) {
102
            throw new ImmutabilityViolationException(\sprintf(
103
                'Class "%s" was already checked for immutability',
104
                static::class
105
            ));
106
        }
107
108
        $this->immutabilityAlreadyChecked = true;
109
    }
110
111
    /**
112
     * Assert immutability check call constraints.
113
     *
114
     * @throws ImmutabilityViolationException
115
     */
116
    private function assertImmutabilityCallConstraints(): void
117
    {
118
        $stack = $this->getFilteredImmutabilityCallStack();
119
120
        $callingMethods = ['__construct', '__wakeup', '__unserialize'];
121
        if ($this instanceof \Serializable) {
122
            $callingMethods[] = 'unserialize';
123
        }
124
125
        if (!isset($stack[1]) || !\in_array($stack[1]['function'], $callingMethods, true)) {
126
            throw new ImmutabilityViolationException(\sprintf(
127
                'Immutability assertion available only through "%s" methods, called from "%s"',
128
                \implode('", "', $callingMethods),
129
                isset($stack[1]) ? static::class . '::' . $stack[1]['function'] : 'unknown'
130
            ));
131
        }
132
    }
133
134
    /**
135
     * Get filter call stack.
136
     *
137
     * @return mixed[]
138
     */
139
    private function getFilteredImmutabilityCallStack(): array
140
    {
141
        $stack = \debug_backtrace();
142
143
        while (\count($stack) > 0 && $stack[0]['function'] !== 'assertImmutable') {
144
            \array_shift($stack);
145
        }
146
147
        if (isset($stack[1]) && $stack[1]['function'] === 'checkImmutability') {
148
            \array_shift($stack);
149
        }
150
151
        return $stack;
152
    }
153
154
    /**
155
     * Check properties visibility.
156
     *
157
     * @throws ImmutabilityViolationException
158
     */
159
    private function assertPropertyVisibility(): void
160
    {
161
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
162
        if (\count($publicProperties) !== 0) {
163
            throw new ImmutabilityViolationException(\sprintf(
164
                'Class "%s" should not have public properties',
165
                static::class
166
            ));
167
        }
168
    }
169
170
    /**
171
     * Check methods visibility.
172
     *
173
     * @throws ImmutabilityViolationException
174
     */
175
    private function assertMethodVisibility(): void
176
    {
177
        $publicMethods = $this->getClassPublicMethods();
178
        $allowedPublicMethods = $this->getAllowedPublicMethods();
179
180
        if (\count($publicMethods) > \count($allowedPublicMethods)
181
            || \count(\array_diff($publicMethods, $allowedPublicMethods)) !== 0
182
        ) {
183
            throw new ImmutabilityViolationException(\sprintf(
184
                'Class "%s" should not have public methods',
185
                static::class
186
            ));
187
        }
188
    }
189
190
    /**
191
     * Get list of defined public methods.
192
     *
193
     * @return string[]
194
     */
195
    private function getClassPublicMethods(): array
196
    {
197
        $publicMethods = \array_filter(\array_map(
198
            function (\ReflectionMethod $method): string {
199
                return !$method->isStatic() ? $method->getName() : '';
200
            },
201
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
202
        ));
203
204
        \sort($publicMethods);
205
206
        return $publicMethods;
207
    }
208
209
    /**
210
     * Get list of allowed public methods.
211
     *
212
     * @return string[]
213
     */
214
    private function getAllowedPublicMethods(): array
215
    {
216
        $allowedInterfaces = \array_unique(\array_filter(\array_merge(
217
            $this->getAllowedInterfaces(),
218
            [ImmutabilityBehaviour::class]
219
        )));
220
        $allowedPublicMethods = \array_unique(\array_filter(\array_merge(
221
            static::$allowedMagicMethods,
222
            ...\array_map(
223
                function (string $interface): array {
224
                    return \array_map(
225
                        function (\ReflectionMethod $method): string {
226
                            return !$method->isStatic() ? $method->getName() : '';
227
                        },
228
                        (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC)
229
                    );
230
                },
231
                $allowedInterfaces
232
            )
233
        )));
234
235
        \sort($allowedPublicMethods);
236
237
        return $allowedPublicMethods;
238
    }
239
240
    /**
241
     * Get a list of allowed interfaces to extract public methods from.
242
     *
243
     * @return string[]
244
     */
245
    abstract protected function getAllowedInterfaces(): array;
246
247
    /**
248
     * @param string  $method
249
     * @param mixed[] $parameters
250
     *
251
     * @return mixed
252
     */
253
    final public function __call(string $method, array $parameters)
254
    {
255
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
256
    }
257
258
    /**
259
     * @param string $name
260
     * @param mixed  $value
261
     *
262
     * @throws ImmutabilityViolationException
263
     */
264
    final public function __set(string $name, $value): void
265
    {
266
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
267
    }
268
269
    /**
270
     * @param string $name
271
     *
272
     * @throws ImmutabilityViolationException
273
     */
274
    final public function __unset(string $name): void
275
    {
276
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
277
    }
278
279
    /**
280
     * @throws ImmutabilityViolationException
281
     */
282
    final public function __invoke(): void
283
    {
284
        throw new ImmutabilityViolationException(\sprintf('Class "%s" invocation is not allowed', static::class));
285
    }
286
}
287