ImmutabilityBehaviour   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 88
dl 0
loc 280
rs 9.92
c 8
b 0
f 0
wmc 31

14 Methods

Rating   Name   Duplication   Size   Complexity  
A checkImmutability() 0 8 1
A getImmutabilityFilteredCallStack() 0 13 5
A getImmutabilityAllowedPublicMethods() 0 24 2
A __unset() 0 3 1
A assertImmutabilityFinal() 0 11 3
A assertImmutabilityPropertyVisibility() 0 6 2
A assertImmutabilitySingleCall() 0 9 2
A __call() 0 3 1
A assertImmutabilityMethodVisibility() 0 10 3
A __set() 0 3 1
A assertImmutabilityCallConstraints() 0 14 5
A __invoke() 0 3 1
A assertImmutable() 0 12 2
A getImmutabilityClassPublicMethods() 0 12 2
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->assertImmutabilitySingleCall();
80
        $this->assertImmutabilityCallConstraints();
81
82
        $class = static::class;
83
        if (!isset(static::$immutabilityCheckMap[$class])) {
84
            $this->assertImmutabilityFinal();
85
            $this->assertImmutabilityPropertyVisibility();
86
            $this->assertImmutabilityMethodVisibility();
87
88
            static::$immutabilityCheckMap[$class] = true;
89
        }
90
    }
91
92
    /**
93
     * Assert single call.
94
     *
95
     * @throws ImmutabilityViolationException
96
     */
97
    private function assertImmutabilitySingleCall(): void
98
    {
99
        if ($this->immutabilityAlreadyChecked) {
100
            throw new ImmutabilityViolationException(
101
                \sprintf('Class "%s" was already checked for immutability.', static::class)
102
            );
103
        }
104
105
        $this->immutabilityAlreadyChecked = true;
106
    }
107
108
    /**
109
     * Assert immutability check call constraints.
110
     *
111
     * @throws ImmutabilityViolationException
112
     */
113
    private function assertImmutabilityCallConstraints(): void
114
    {
115
        $stack = $this->getImmutabilityFilteredCallStack();
116
117
        $callingMethods = ['__construct', '__wakeup', '__unserialize'];
118
        if ($this instanceof \Serializable) {
119
            $callingMethods[] = 'unserialize';
120
        }
121
122
        if (!isset($stack[1]) || !\in_array($stack[1]['function'], $callingMethods, true)) {
123
            throw new ImmutabilityViolationException(\sprintf(
124
                'Immutability assertion available only through "%s" methods, called from "%s".',
125
                \implode('", "', $callingMethods),
126
                isset($stack[1]) ? static::class . '::' . $stack[1]['function'] : 'unknown'
127
            ));
128
        }
129
    }
130
131
    /**
132
     * Get filter call stack.
133
     *
134
     * @return mixed[]
135
     */
136
    private function getImmutabilityFilteredCallStack(): array
137
    {
138
        $stack = \debug_backtrace();
139
140
        while (\count($stack) > 0 && $stack[0]['function'] !== 'assertImmutable') {
141
            \array_shift($stack);
142
        }
143
144
        if (isset($stack[1]) && $stack[1]['function'] === 'checkImmutability') {
145
            \array_shift($stack);
146
        }
147
148
        return $stack;
149
    }
150
151
    /**
152
     * Assert final.
153
     *
154
     * @throws ImmutabilityViolationException
155
     */
156
    private function assertImmutabilityFinal(): void
157
    {
158
        $reflectionObject = new \ReflectionObject($this);
159
160
        if (!$reflectionObject->isFinal()) {
161
            $reflectionMethod = $reflectionObject->getMethod('getAllowedInterfaces');
162
163
            if (!$reflectionMethod->isFinal()) {
164
                throw new ImmutabilityViolationException(\sprintf(
165
                    'Class "%s" or getAllowedInterfaces method should be final.',
166
                    static::class
167
                ));
168
            }
169
        }
170
    }
171
172
    /**
173
     * Assert properties visibility.
174
     *
175
     * @throws ImmutabilityViolationException
176
     */
177
    private function assertImmutabilityPropertyVisibility(): void
178
    {
179
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
180
        if (\count($publicProperties) !== 0) {
181
            throw new ImmutabilityViolationException(
182
                \sprintf('Class "%s" should not have public properties.', static::class)
183
            );
184
        }
185
    }
186
187
    /**
188
     * Assert methods visibility.
189
     *
190
     * @throws ImmutabilityViolationException
191
     */
192
    private function assertImmutabilityMethodVisibility(): void
193
    {
194
        $publicMethods = $this->getImmutabilityClassPublicMethods();
195
        $allowedPublicMethods = $this->getImmutabilityAllowedPublicMethods();
196
197
        if (\count($publicMethods) > \count($allowedPublicMethods)
198
            || \count(\array_diff($publicMethods, $allowedPublicMethods)) !== 0
199
        ) {
200
            throw new ImmutabilityViolationException(
201
                \sprintf('Class "%s" should not have public methods.', static::class)
202
            );
203
        }
204
    }
205
206
    /**
207
     * Get list of defined public methods.
208
     *
209
     * @return string[]
210
     */
211
    private function getImmutabilityClassPublicMethods(): array
212
    {
213
        $publicMethods = \array_filter(\array_map(
214
            function (\ReflectionMethod $method): string {
215
                return !$method->isStatic() ? $method->getName() : '';
216
            },
217
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
218
        ));
219
220
        \sort($publicMethods);
221
222
        return $publicMethods;
223
    }
224
225
    /**
226
     * Get list of allowed public methods.
227
     *
228
     * @return string[]
229
     */
230
    private function getImmutabilityAllowedPublicMethods(): array
231
    {
232
        $allowedInterfaces = \array_unique(\array_filter(\array_merge(
233
            $this->getAllowedInterfaces(),
234
            [ImmutabilityBehaviour::class]
235
        )));
236
        $allowedPublicMethods = \array_unique(\array_filter(\array_merge(
237
            static::$allowedMagicMethods,
238
            ...\array_map(
239
                function (string $interface): array {
240
                    return \array_map(
241
                        function (\ReflectionMethod $method): string {
242
                            return !$method->isStatic() ? $method->getName() : '';
243
                        },
244
                        (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC)
245
                    );
246
                },
247
                $allowedInterfaces
248
            )
249
        )));
250
251
        \sort($allowedPublicMethods);
252
253
        return $allowedPublicMethods;
254
    }
255
256
    /**
257
     * Get a list of allowed interfaces to extract public methods from.
258
     *
259
     * @return string[]
260
     */
261
    abstract protected function getAllowedInterfaces(): array;
262
263
    /**
264
     * @param string  $method
265
     * @param mixed[] $parameters
266
     *
267
     * @return mixed
268
     */
269
    final public function __call(string $method, array $parameters)
270
    {
271
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated.', static::class));
272
    }
273
274
    /**
275
     * @param string $name
276
     * @param mixed  $value
277
     *
278
     * @throws ImmutabilityViolationException
279
     */
280
    final public function __set(string $name, $value): void
281
    {
282
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated.', static::class));
283
    }
284
285
    /**
286
     * @param string $name
287
     *
288
     * @throws ImmutabilityViolationException
289
     */
290
    final public function __unset(string $name): void
291
    {
292
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated.', static::class));
293
    }
294
295
    /**
296
     * @throws ImmutabilityViolationException
297
     */
298
    final public function __invoke(): void
299
    {
300
        throw new ImmutabilityViolationException(\sprintf('Class "%s" invocation is not allowed.', static::class));
301
    }
302
}
303