Completed
Push — master ( a26aca...911be7 )
by Julián
03:25
created

getImmutabilityAllowedPublicMethods()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
nc 1
nop 0
dl 0
loc 24
rs 9.7998
c 0
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->assertImmutabilitySingleCall();
80
81
        $class = static::class;
82
83
        if (isset(static::$immutabilityCheckMap[$class])) {
84
            return;
85
        }
86
87
        $this->assertImmutabilityCallConstraints();
88
        $this->assertImmutabilityPropertyVisibility();
89
        $this->assertImmutabilityMethodVisibility();
90
91
        static::$immutabilityCheckMap[$class] = true;
92
    }
93
94
    /**
95
     * Assert single call.
96
     *
97
     * @throws ImmutabilityViolationException
98
     */
99
    private function assertImmutabilitySingleCall(): void
100
    {
101
        if ($this->immutabilityAlreadyChecked) {
102
            throw new ImmutabilityViolationException(
103
                \sprintf('Class "%s" was already checked for immutability', static::class)
104
            );
105
        }
106
107
        $this->immutabilityAlreadyChecked = true;
108
    }
109
110
    /**
111
     * Assert immutability check call constraints.
112
     *
113
     * @throws ImmutabilityViolationException
114
     */
115
    private function assertImmutabilityCallConstraints(): void
116
    {
117
        $stack = $this->getImmutabilityFilteredCallStack();
118
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 assertion 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 getImmutabilityFilteredCallStack(): array
139
    {
140
        $stack = \debug_backtrace();
141
142
        while (\count($stack) > 0 && $stack[0]['function'] !== 'assertImmutable') {
143
            \array_shift($stack);
144
        }
145
146
        if (isset($stack[1]) && $stack[1]['function'] === 'checkImmutability') {
147
            \array_shift($stack);
148
        }
149
150
        return $stack;
151
    }
152
153
    /**
154
     * Check properties visibility.
155
     *
156
     * @throws ImmutabilityViolationException
157
     */
158
    private function assertImmutabilityPropertyVisibility(): void
159
    {
160
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
161
        if (\count($publicProperties) !== 0) {
162
            throw new ImmutabilityViolationException(
163
                \sprintf('Class "%s" should not have public properties', static::class)
164
            );
165
        }
166
    }
167
168
    /**
169
     * Check methods visibility.
170
     *
171
     * @throws ImmutabilityViolationException
172
     */
173
    private function assertImmutabilityMethodVisibility(): void
174
    {
175
        $publicMethods = $this->getImmutabilityClassPublicMethods();
176
        $allowedPublicMethods = $this->getImmutabilityAllowedPublicMethods();
177
178
        if (\count($publicMethods) > \count($allowedPublicMethods)
179
            || \count(\array_diff($publicMethods, $allowedPublicMethods)) !== 0
180
        ) {
181
            throw new ImmutabilityViolationException(
182
                \sprintf('Class "%s" should not have public methods', static::class)
183
            );
184
        }
185
    }
186
187
    /**
188
     * Get list of defined public methods.
189
     *
190
     * @return string[]
191
     */
192
    private function getImmutabilityClassPublicMethods(): array
193
    {
194
        $publicMethods = \array_filter(\array_map(
195
            function (\ReflectionMethod $method): string {
196
                return !$method->isStatic() ? $method->getName() : '';
197
            },
198
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
199
        ));
200
201
        \sort($publicMethods);
202
203
        return $publicMethods;
204
    }
205
206
    /**
207
     * Get list of allowed public methods.
208
     *
209
     * @return string[]
210
     */
211
    private function getImmutabilityAllowedPublicMethods(): array
212
    {
213
        $allowedInterfaces = \array_unique(\array_filter(\array_merge(
214
            $this->getAllowedInterfaces(),
215
            [ImmutabilityBehaviour::class]
216
        )));
217
        $allowedPublicMethods = \array_unique(\array_filter(\array_merge(
218
            static::$allowedMagicMethods,
219
            ...\array_map(
220
                function (string $interface): array {
221
                    return \array_map(
222
                        function (\ReflectionMethod $method): string {
223
                            return !$method->isStatic() ? $method->getName() : '';
224
                        },
225
                        (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC)
226
                    );
227
                },
228
                $allowedInterfaces
229
            )
230
        )));
231
232
        \sort($allowedPublicMethods);
233
234
        return $allowedPublicMethods;
235
    }
236
237
    /**
238
     * Get a list of allowed interfaces to extract public methods from.
239
     *
240
     * @return string[]
241
     */
242
    abstract protected function getAllowedInterfaces(): array;
243
244
    /**
245
     * @param string  $method
246
     * @param mixed[] $parameters
247
     *
248
     * @return mixed
249
     */
250
    final public function __call(string $method, array $parameters)
251
    {
252
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
253
    }
254
255
    /**
256
     * @param string $name
257
     * @param mixed  $value
258
     *
259
     * @throws ImmutabilityViolationException
260
     */
261
    final public function __set(string $name, $value): void
262
    {
263
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
264
    }
265
266
    /**
267
     * @param string $name
268
     *
269
     * @throws ImmutabilityViolationException
270
     */
271
    final public function __unset(string $name): void
272
    {
273
        throw new ImmutabilityViolationException(\sprintf('Class "%s" properties cannot be mutated', static::class));
274
    }
275
276
    /**
277
     * @throws ImmutabilityViolationException
278
     */
279
    final public function __invoke(): void
280
    {
281
        throw new ImmutabilityViolationException(\sprintf('Class "%s" invocation is not allowed', static::class));
282
    }
283
}
284