Completed
Push — master ( 1b818a...a34bf8 )
by Julián
01:23
created

ImmutabilityBehaviour::checkConstructCall()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
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
    protected $alreadyConstructed = false;
54
55
    /**
56
     * Check immutability.
57
     *
58
     * @throws ImmutabilityViolationException
59
     */
60
    final protected function checkImmutability(): void
61
    {
62
        $class = static::class;
63
64
        $this->checkConstructCall();
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 is called only once.
78
     *
79
     * @throws ImmutabilityViolationException
80
     */
81
    final protected function checkConstructCall(): void
82
    {
83
        if ($this->alreadyConstructed) {
84
            throw new ImmutabilityViolationException(\sprintf(
85
                'Method %s::__construct was already called',
86
                static::class
87
            ));
88
        }
89
90
        $this->alreadyConstructed = true;
91
    }
92
93
    /**
94
     * Check properties accessibility.
95
     *
96
     * @throws ImmutabilityViolationException
97
     */
98
    final protected function checkPropertiesAccessibility(): void
99
    {
100
        $publicProperties = (new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC);
101
        if (\count($publicProperties) !== 0) {
102
            throw new ImmutabilityViolationException(\sprintf(
103
                'Class %s should not have public properties',
104
                static::class
105
            ));
106
        }
107
    }
108
109
    /**
110
     * Check methods accessibility.
111
     *
112
     * @throws ImmutabilityViolationException
113
     */
114
    final protected function checkMethodsAccessibility(): void
115
    {
116
        $publicMethods = $this->getClassPublicMethods();
117
        \sort($publicMethods);
118
119
        $allowedPublicMethods = $this->getAllowedPublicMethods();
120
121
        foreach (static::$allowedMagicMethods as $magicMethod) {
122
            if (\array_search($magicMethod, $publicMethods, true) !== false) {
123
                $allowedPublicMethods[] = $magicMethod;
124
            }
125
        }
126
127
        \sort($allowedPublicMethods);
128
129
        if (\count($publicMethods) > \count($allowedPublicMethods)
130
            || \count(\array_diff($allowedPublicMethods, $publicMethods)) !== 0
131
        ) {
132
            throw new ImmutabilityViolationException(\sprintf(
133
                'Class %s should not have public methods',
134
                static::class
135
            ));
136
        }
137
    }
138
139
    /**
140
     * Get list of defined public methods.
141
     *
142
     * @return string[]
143
     */
144
    final protected function getClassPublicMethods(): array
145
    {
146
        return \array_filter(\array_map(
147
            function (\ReflectionMethod $method): string {
148
                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...
149
            },
150
            (new \ReflectionObject($this))->getMethods(\ReflectionMethod::IS_PUBLIC)
151
        ));
152
    }
153
154
    /**
155
     * Get list of allowed public methods.
156
     *
157
     * @return string[]
158
     */
159
    protected function getAllowedPublicMethods(): array
160
    {
161
        $allowedInterfaces = \array_unique(\array_merge($this->getAllowedInterfaces(), [ImmutabilityBehaviour::class]));
162
        $allowedMethods = \array_merge(
163
            ...\array_map(
164
                function (string $interface): array {
165
                    return (new \ReflectionClass($interface))->getMethods(\ReflectionMethod::IS_PUBLIC);
166
                },
167
                $allowedInterfaces
168
            )
169
        );
170
171
        return \array_unique(\array_filter(\array_map(
172
            function (\ReflectionMethod $method): string {
173
                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...
174
            },
175
            $allowedMethods
176
        )));
177
    }
178
179
    /**
180
     * Get a list of allowed interfaces to extract public methods from.
181
     *
182
     * @return string[]
183
     */
184
    abstract protected function getAllowedInterfaces(): array;
185
186
    /**
187
     * @param string  $method
188
     * @param mixed[] $parameters
189
     *
190
     * @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...
191
     */
192
    final public function __call(string $method, array $parameters)
193
    {
194
        throw new ImmutabilityViolationException(\sprintf('Class %s properties cannot be mutated', static::class));
195
    }
196
197
    /**
198
     * @param string $name
199
     * @param mixed  $value
200
     *
201
     * @throws ImmutabilityViolationException
202
     */
203
    final public function __set(string $name, $value): void
204
    {
205
        throw new ImmutabilityViolationException(\sprintf('Class %s properties cannot be mutated', static::class));
206
    }
207
208
    /**
209
     * @param string $name
210
     *
211
     * @throws ImmutabilityViolationException
212
     */
213
    final public function __unset(string $name): void
214
    {
215
        throw new ImmutabilityViolationException(\sprintf('Class %s properties cannot be mutated', static::class));
216
    }
217
218
    /**
219
     * @throws ImmutabilityViolationException
220
     */
221
    final public function __invoke(): void
222
    {
223
        throw new ImmutabilityViolationException('Invocation is not allowed');
224
    }
225
}
226