Completed
Push — master ( e57ae0...18c749 )
by Julián
06:59
created

ImmutabilityBehaviour::assertMethodVisibility()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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