Completed
Push — master ( 685d5a...cc0b43 )
by Julián
05:02
created

ImmutabilityBehaviour::assertMethodsVisibility()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
c 0
b 0
f 0
nc 6
nop 0
dl 0
loc 21
rs 9.5555
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
        '__toString',
43
        '__set_state',
44
        '__clone',
45
        '__debugInfo',
46
    ];
47
48
    /**
49
     * Single constructor call check.
50
     *
51
     * @var bool
52
     */
53
    private $immutabilityAlreadyChecked = false;
54
55
    /**
56
     * Alias of assertImmutable.
57
     *
58
     * @deprecated use assertImmutable instead
59
     */
60
    final protected function checkImmutability(): void
61
    {
62
        @\trigger_error(
63
            'Calling the "checkImmutability()" method is deprecated. Use "assertImmutable()" method instead',
64
            \E_USER_DEPRECATED
65
        );
66
67
        $this->assertImmutable();
68
    }
69
70
    /**
71
     * Assert object immutability.
72
     *
73
     * @throws ImmutabilityViolationException
74
     */
75
    final protected function assertImmutable(): void
76
    {
77
        $this->assertSingleCheck();
78
79
        $class = static::class;
80
81
        if (isset(static::$immutabilityCheckMap[$class])) {
82
            return;
83
        }
84
85
        $this->assertCallConstraints();
86
        $this->assertPropertiesVisibility();
87
        $this->assertMethodsVisibility();
88
89
        $this->immutabilityAlreadyChecked = true;
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
109
    /**
110
     * Assert immutability check call constraints.
111
     *
112
     * @throws ImmutabilityViolationException
113
     */
114
    private function assertCallConstraints(): void
115
    {
116
        $serializable = \in_array(\Serializable::class, \class_implements($this), true);
117
        $stack = $this->getFilteredCallStack();
118
119
        if (!isset($stack[1])
120
            || ($serializable && !\in_array($stack[1]['function'], ['__construct', 'unserialize'], true))
121
            || (!$serializable && $stack[1]['function'] !== '__construct')
122
        ) {
123
            throw new ImmutabilityViolationException(\sprintf(
124
                'Immutability check must be called from constructor or "unserialize" methods, called from "%s"',
125
                isset($stack[1]) ? static::class . '::' . $stack[1]['function'] : 'unknown'
126
            ));
127
        }
128
    }
129
130
    /**
131
     * Get filter call stack.
132
     *
133
     * @return mixed[]
134
     */
135
    private function getFilteredCallStack(): array
136
    {
137
        $stack = \debug_backtrace();
138
        while (\count($stack) > 0 && $stack[0]['function'] !== 'assertImmutable') {
139
            \array_shift($stack);
140
        }
141
142
        if (isset($stack[1]) && $stack[1]['function'] === 'checkImmutability') {
143
            \array_shift($stack);
144
        }
145
146
        return $stack;
147
    }
148
149
    /**
150
     * Check properties visibility.
151
     *
152
     * @throws ImmutabilityViolationException
153
     */
154
    private function assertPropertiesVisibility(): void
155
    {
156
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
157
        if (\count($publicProperties) !== 0) {
158
            throw new ImmutabilityViolationException(\sprintf(
159
                'Class "%s" should not have public properties',
160
                static::class
161
            ));
162
        }
163
    }
164
165
    /**
166
     * Check methods visibility.
167
     *
168
     * @throws ImmutabilityViolationException
169
     */
170
    private function assertMethodsVisibility(): void
171
    {
172
        $publicMethods = $this->getClassPublicMethods();
173
        \sort($publicMethods);
174
175
        $allowedPublicMethods = $this->getAllowedPublicMethods();
176
177
        foreach (static::$allowedMagicMethods as $magicMethod) {
178
            if (\in_array($magicMethod, $publicMethods, true)) {
179
                $allowedPublicMethods[] = $magicMethod;
180
            }
181
        }
182
183
        \sort($allowedPublicMethods);
184
185
        if (\count($publicMethods) > \count($allowedPublicMethods)
186
            || \count(\array_diff($allowedPublicMethods, $publicMethods)) !== 0
187
        ) {
188
            throw new ImmutabilityViolationException(\sprintf(
189
                'Class "%s" should not have public methods',
190
                static::class
191
            ));
192
        }
193
    }
194
195
    /**
196
     * Get list of defined public methods.
197
     *
198
     * @return string[]
199
     */
200
    private function getClassPublicMethods(): array
201
    {
202
        return \array_filter(\array_map(
203
            function (\ReflectionMethod $method): string {
204
                return !$method->isStatic() ? $method->getName() : '';
205
            },
206
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
207
        ));
208
    }
209
210
    /**
211
     * Get list of allowed public methods.
212
     *
213
     * @return string[]
214
     */
215
    private function getAllowedPublicMethods(): array
216
    {
217
        $allowedInterfaces = \array_unique(\array_merge($this->getAllowedInterfaces(), [ImmutabilityBehaviour::class]));
218
        $allowedMethods = \array_merge(
219
            ...\array_map(
220
                function (string $interface): array {
221
                    return (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC);
222
                },
223
                $allowedInterfaces
224
            )
225
        );
226
227
        return \array_unique(\array_filter(\array_map(
228
            function (\ReflectionMethod $method): string {
229
                return !$method->isStatic() ? $method->getName() : '';
230
            },
231
            $allowedMethods
232
        )));
233
    }
234
235
    /**
236
     * Get a list of allowed interfaces to extract public methods from.
237
     *
238
     * @return string[]
239
     */
240
    abstract protected function getAllowedInterfaces(): array;
241
242
    /**
243
     * @param string  $method
244
     * @param mixed[] $parameters
245
     *
246
     * @return mixed
247
     */
248
    final public function __call(string $method, array $parameters)
249
    {
250
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
251
    }
252
253
    /**
254
     * @param string $name
255
     * @param mixed  $value
256
     *
257
     * @throws ImmutabilityViolationException
258
     */
259
    final public function __set(string $name, $value): void
260
    {
261
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
262
    }
263
264
    /**
265
     * @param string $name
266
     *
267
     * @throws ImmutabilityViolationException
268
     */
269
    final public function __unset(string $name): void
270
    {
271
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
272
    }
273
274
    /**
275
     * @throws ImmutabilityViolationException
276
     */
277
    final public function __invoke(): void
278
    {
279
        throw new ImmutabilityViolationException('Invocation is not allowed');
280
    }
281
}
282