Completed
Push — master ( 87f03a...80aa37 )
by Julián
01:25
created

ImmutabilityBehaviour::checkCallConstraints()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9137
c 0
b 0
f 0
cc 6
nc 5
nop 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
        '__toString',
43
        '__set_state',
44
        '__clone',
45
        '__debugInfo',
46
    ];
47
48
    /**
49
     * Single constructor call check.
50
     *
51
     * @var bool
52
     */
53
    private $alreadyConstructed = false;
54
55
    /**
56
     * Check immutability.
57
     *
58
     * @throws ImmutabilityViolationException
59
     */
60
    final protected function checkImmutability(): void
61
    {
62
        $this->checkCallConstraints();
63
64
        $class = static::class;
65
66
        if (isset(static::$immutabilityCheckMap[$class])) {
67
            return;
68
        }
69
70
        $this->checkPropertiesAccessibility();
71
        $this->checkMethodsAccessibility();
72
73
        static::$immutabilityCheckMap[$class] = true;
74
    }
75
76
    /**
77
     * Check __construct method constraints.
78
     *
79
     * @throws ImmutabilityViolationException
80
     */
81
    private function checkCallConstraints(): void
82
    {
83
        if ($this->alreadyConstructed) {
84
            throw new ImmutabilityViolationException(\sprintf(
85
                'Class %s constructor was already called',
86
                static::class
87
            ));
88
        }
89
90
        $stack = \debug_backtrace();
91
        while (\count($stack) > 0 && $stack[0]['function'] !== 'checkImmutability') {
92
            \array_shift($stack);
93
        }
94
95
        if (!isset($stack[1]) || $stack[1]['function'] !== '__construct') {
96
            throw new ImmutabilityViolationException(\sprintf(
97
                'Immutability check can only be called from constructor, called from %s::%s',
98
                static::class,
99
                $stack[1]['function']
100
            ));
101
        }
102
103
        $this->alreadyConstructed = true;
104
    }
105
106
    /**
107
     * Check properties accessibility.
108
     *
109
     * @throws ImmutabilityViolationException
110
     */
111
    private function checkPropertiesAccessibility(): void
112
    {
113
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
114
        if (\count($publicProperties) !== 0) {
115
            throw new ImmutabilityViolationException(\sprintf(
116
                'Class %s should not have public properties',
117
                static::class
118
            ));
119
        }
120
    }
121
122
    /**
123
     * Check methods accessibility.
124
     *
125
     * @throws ImmutabilityViolationException
126
     */
127
    private function checkMethodsAccessibility(): void
128
    {
129
        $publicMethods = $this->getClassPublicMethods();
130
        \sort($publicMethods);
131
132
        $allowedPublicMethods = $this->getAllowedPublicMethods();
133
134
        foreach (static::$allowedMagicMethods as $magicMethod) {
135
            if (\array_search($magicMethod, $publicMethods, true) !== false) {
136
                $allowedPublicMethods[] = $magicMethod;
137
            }
138
        }
139
140
        \sort($allowedPublicMethods);
141
142
        if (\count($publicMethods) > \count($allowedPublicMethods)
143
            || \count(\array_diff($allowedPublicMethods, $publicMethods)) !== 0
144
        ) {
145
            throw new ImmutabilityViolationException(\sprintf(
146
                'Class %s should not have public methods',
147
                static::class
148
            ));
149
        }
150
    }
151
152
    /**
153
     * Get list of defined public methods.
154
     *
155
     * @return string[]
156
     */
157
    private function getClassPublicMethods(): array
158
    {
159
        return \array_filter(\array_map(
160
            function (\ReflectionMethod $method): string {
161
                return !$method->isStatic() ? $method->getName() : '';
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
162
            },
163
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
164
        ));
165
    }
166
167
    /**
168
     * Get list of allowed public methods.
169
     *
170
     * @return string[]
171
     */
172
    protected function getAllowedPublicMethods(): array
173
    {
174
        $allowedInterfaces = \array_unique(\array_merge($this->getAllowedInterfaces(), [ImmutabilityBehaviour::class]));
175
        $allowedMethods = \array_merge(
176
            ...\array_map(
177
                function (string $interface): array {
178
                    return (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC);
179
                },
180
                $allowedInterfaces
181
            )
182
        );
183
184
        return \array_unique(\array_filter(\array_map(
185
            function (\ReflectionMethod $method): string {
186
                return !$method->isStatic() ? $method->getName() : '';
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
187
            },
188
            $allowedMethods
189
        )));
190
    }
191
192
    /**
193
     * Get a list of allowed interfaces to extract public methods from.
194
     *
195
     * @return string[]
196
     */
197
    abstract protected function getAllowedInterfaces(): array;
198
199
    /**
200
     * @param string  $method
201
     * @param mixed[] $parameters
202
     *
203
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use NoType.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
204
     */
205
    final public function __call(string $method, array $parameters)
206
    {
207
        throw new ImmutabilityViolationException(\sprintf('Class %s properties cannot be mutated', static::class));
208
    }
209
210
    /**
211
     * @param string $name
212
     * @param mixed  $value
213
     *
214
     * @throws ImmutabilityViolationException
215
     */
216
    final public function __set(string $name, $value): void
217
    {
218
        throw new ImmutabilityViolationException(\sprintf('Class %s properties cannot be mutated', static::class));
219
    }
220
221
    /**
222
     * @param string $name
223
     *
224
     * @throws ImmutabilityViolationException
225
     */
226
    final public function __unset(string $name): void
227
    {
228
        throw new ImmutabilityViolationException(\sprintf('Class %s properties cannot be mutated', static::class));
229
    }
230
231
    /**
232
     * @throws ImmutabilityViolationException
233
     */
234
    final public function __invoke(): void
235
    {
236
        throw new ImmutabilityViolationException('Invocation is not allowed');
237
    }
238
}
239