Completed
Push — master ( 130a83...ad9b26 )
by Julián
02:55
created

ImmutabilityBehaviour::getAllowedCallingMethods()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
rs 10
c 1
b 0
f 0
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
        $callingMethods = $this->getAllowedCallingMethods();
120
121
        if (!isset($stack[1]) || !\in_array($stack[1]['function'], $callingMethods, true)) {
122
            throw new ImmutabilityViolationException(\sprintf(
123
                'Immutability assertion available only through "%s" methods, called from "%s"',
124
                \implode('", "', $callingMethods),
125
                isset($stack[1]) ? static::class . '::' . $stack[1]['function'] : 'unknown'
126
            ));
127
        }
128
    }
129
130
    /**
131
     * Get allowed calling methods.
132
     *
133
     * @return string[]
134
     */
135
    private function getAllowedCallingMethods(): array
136
    {
137
        $callingMethods = ['__construct', '__wakeup', '__unserialize'];
138
        if ($this instanceof \Serializable) {
139
            $callingMethods[] = 'unserialize';
140
        }
141
142
        return $callingMethods;
143
    }
144
145
    /**
146
     * Get filter call stack.
147
     *
148
     * @return mixed[]
149
     */
150
    private function getFilteredImmutabilityCallStack(): array
151
    {
152
        $stack = \debug_backtrace();
153
154
        while (\count($stack) > 0 && $stack[0]['function'] !== 'assertImmutable') {
155
            \array_shift($stack);
156
        }
157
158
        if (isset($stack[1]) && $stack[1]['function'] === 'checkImmutability') {
159
            \array_shift($stack);
160
        }
161
162
        return $stack;
163
    }
164
165
    /**
166
     * Check properties visibility.
167
     *
168
     * @throws ImmutabilityViolationException
169
     */
170
    private function assertPropertyVisibility(): void
171
    {
172
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
173
        if (\count($publicProperties) !== 0) {
174
            throw new ImmutabilityViolationException(\sprintf(
175
                'Class "%s" should not have public properties',
176
                static::class
177
            ));
178
        }
179
    }
180
181
    /**
182
     * Check methods visibility.
183
     *
184
     * @throws ImmutabilityViolationException
185
     */
186
    private function assertMethodVisibility(): void
187
    {
188
        $publicMethods = $this->getClassPublicMethods();
189
        $allowedPublicMethods = $this->getAllowedPublicMethods();
190
191
        if (\count($publicMethods) > \count($allowedPublicMethods)
192
            || \count(\array_diff($publicMethods, $allowedPublicMethods)) !== 0
193
        ) {
194
            throw new ImmutabilityViolationException(\sprintf(
195
                'Class "%s" should not have public methods',
196
                static::class
197
            ));
198
        }
199
    }
200
201
    /**
202
     * Get list of defined public methods.
203
     *
204
     * @return string[]
205
     */
206
    private function getClassPublicMethods(): array
207
    {
208
        $publicMethods = \array_filter(\array_map(
209
            function (\ReflectionMethod $method): string {
210
                return !$method->isStatic() ? $method->getName() : '';
211
            },
212
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
213
        ));
214
215
        \sort($publicMethods);
216
217
        return $publicMethods;
218
    }
219
220
    /**
221
     * Get list of allowed public methods.
222
     *
223
     * @return string[]
224
     */
225
    private function getAllowedPublicMethods(): array
226
    {
227
        $allowedInterfaces = \array_unique(\array_filter(\array_merge(
228
            $this->getAllowedInterfaces(),
229
            [ImmutabilityBehaviour::class]
230
        )));
231
        $allowedPublicMethods = \array_unique(\array_filter(\array_merge(
232
            static::$allowedMagicMethods,
233
            ...\array_map(
234
                function (string $interface): array {
235
                    return \array_map(
236
                        function (\ReflectionMethod $method): string {
237
                            return !$method->isStatic() ? $method->getName() : '';
238
                        },
239
                        (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC)
240
                    );
241
                },
242
                $allowedInterfaces
243
            )
244
        )));
245
246
        \sort($allowedPublicMethods);
247
248
        return $allowedPublicMethods;
249
    }
250
251
    /**
252
     * Get a list of allowed interfaces to extract public methods from.
253
     *
254
     * @return string[]
255
     */
256
    abstract protected function getAllowedInterfaces(): array;
257
258
    /**
259
     * @param string  $method
260
     * @param mixed[] $parameters
261
     *
262
     * @return mixed
263
     */
264
    final public function __call(string $method, array $parameters)
265
    {
266
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
267
    }
268
269
    /**
270
     * @param string $name
271
     * @param mixed  $value
272
     *
273
     * @throws ImmutabilityViolationException
274
     */
275
    final public function __set(string $name, $value): void
276
    {
277
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
278
    }
279
280
    /**
281
     * @param string $name
282
     *
283
     * @throws ImmutabilityViolationException
284
     */
285
    final public function __unset(string $name): void
286
    {
287
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
288
    }
289
290
    /**
291
     * @throws ImmutabilityViolationException
292
     */
293
    final public function __invoke(): void
294
    {
295
        throw new ImmutabilityViolationException(\sprintf('Class "%s" invocation is not allowed', static::class));
296
    }
297
}
298