GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Prototype::__construct()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 6
nc 4
nop 1
dl 0
loc 11
rs 10
c 1
b 0
f 1
1
<?php
2
3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ICanBoogie;
13
14
use ArrayAccess;
15
use ArrayIterator;
16
use ICanBoogie\Prototype\MethodNotDefined;
17
use IteratorAggregate;
18
use ReturnTypeWillChange;
0 ignored issues
show
Bug introduced by
The type ReturnTypeWillChange was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Traversable;
20
21
use function array_diff_key;
22
use function array_intersect_key;
23
use function array_merge;
24
use function get_class;
25
use function get_parent_class;
26
use function is_object;
27
use function is_subclass_of;
28
29
/**
30
 * Manages the prototype methods that may be bound to classes using {@link PrototypeTrait}.
31
 *
32
 * @implements ArrayAccess<string, callable>
33
 * @implements IteratorAggregate<string, callable>
34
 */
35
final class Prototype implements ArrayAccess, IteratorAggregate
36
{
37
    /**
38
     * Prototypes instances per class.
39
     *
40
     * @var array<string, Prototype>
41
     */
42
    private static $prototypes = [];
43
44
    /**
45
     * Prototype methods per class.
46
     *
47
     * @var array<class-string, array<string, callable>>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, array<string, callable>>|null at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, array<string, callable>>|null.
Loading history...
48
     */
49
    private static $bindings;
50
51
    /**
52
     * Returns the prototype associated with the specified class or object.
53
     *
54
     * @param class-string|object $class_or_object Class name or object.
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|object.
Loading history...
55
     */
56
    public static function from($class_or_object): Prototype
57
    {
58
        $class = is_object($class_or_object) ? get_class($class_or_object) : $class_or_object;
59
        $prototype = &self::$prototypes[$class];
60
61
        /** @phpstan-ignore-next-line */
62
        return $prototype ?? $prototype = new self($class);
63
    }
64
65
    /**
66
     * Defines prototype methods.
67
     *
68
     * @param array<class-string, array<string, callable>> $bindings
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, array<string, callable>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, array<string, callable>>.
Loading history...
69
     */
70
    public static function bind(array $bindings): void
71
    {
72
        if (!$bindings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $bindings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
73
            return;
74
        }
75
76
        self::update_bindings($bindings);
77
        self::update_instances($bindings);
78
    }
79
80
    /**
81
     * Updates prototype methods with bindings.
82
     *
83
     * @param array<class-string, array<string, callable>> $bindings
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, array<string, callable>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, array<string, callable>>.
Loading history...
84
     */
85
    private static function update_bindings(array $bindings): void
86
    {
87
        $current = &self::$bindings;
88
89
        if (!$current) {
90
            $current = $bindings;
91
        }
92
93
        $intersect = array_intersect_key($bindings, $current);
94
        $current += array_diff_key($bindings, $current);
95
96
        foreach ($intersect as $class => $methods) {
97
            $current[$class] = array_merge($current[$class], $methods);
98
        }
99
    }
100
101
    /**
102
     * Updates instances with bindings.
103
     *
104
     * @param array<class-string, array<string, callable>> $bindings
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, array<string, callable>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, array<string, callable>>.
Loading history...
105
     */
106
    private static function update_instances(array $bindings): void
107
    {
108
        foreach (self::$prototypes as $class => $prototype) {
109
            $prototype->consolidated_methods = null;
110
111
            if (empty($bindings[$class])) {
112
                continue;
113
            }
114
115
            $prototype->methods = $bindings[$class] + $prototype->methods;
116
        }
117
    }
118
119
    /**
120
     * Class associated with the prototype.
121
     *
122
     * @var class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
123
     */
124
    private $class;
125
126
    /**
127
     * Parent prototype.
128
     *
129
     * @var Prototype|null
130
     */
131
    private $parent;
132
133
    /**
134
     * Methods defined by the prototype.
135
     *
136
     * @var array<string, callable>
137
     */
138
    private $methods = [];
139
140
    /**
141
     * Methods defined by the prototypes chain.
142
     *
143
     * @var array<string, callable>|null
144
     */
145
    private $consolidated_methods;
146
147
    /**
148
     * Creates a prototype for the specified class.
149
     *
150
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
151
     */
152
    private function __construct(string $class)
153
    {
154
        $this->class = $class;
155
        $parent_class = get_parent_class($class);
156
157
        if ($parent_class) {
158
            $this->parent = self::from($parent_class);
159
        }
160
161
        if (isset(self::$bindings[$class])) {
162
            $this->methods = self::$bindings[$class];
163
        }
164
    }
165
166
    /**
167
     * Returns the consolidated methods of the prototype.
168
     *
169
     * @return array<string, callable>
170
     */
171
    private function get_consolidated_methods(): array
172
    {
173
        $consolidated_methods = &$this->consolidated_methods;
174
175
        if ($consolidated_methods !== null) {
176
            return $consolidated_methods;
177
        }
178
179
        return $consolidated_methods = $this->consolidate_methods();
180
    }
181
182
    /**
183
     * Consolidate the methods of the prototype.
184
     *
185
     * The method creates a single array from the prototype methods and those of its parents.
186
     *
187
     * @return array<string, callable>
188
     */
189
    private function consolidate_methods(): array
190
    {
191
        $methods = $this->methods;
192
193
        if ($this->parent) {
194
            $methods += $this->parent->get_consolidated_methods();
195
        }
196
197
        return $methods;
198
    }
199
200
    /**
201
     * Revokes the consolidated methods of the prototype.
202
     *
203
     * The method must be invoked when prototype methods are modified.
204
     */
205
    private function revoke_consolidated_methods(): void
206
    {
207
        $class = $this->class;
208
209
        foreach (self::$prototypes as $prototype) {
210
            if (!is_subclass_of($prototype->class, $class)) {
211
                continue;
212
            }
213
214
            $prototype->consolidated_methods = null;
215
        }
216
217
        $this->consolidated_methods = null;
218
    }
219
220
    /**
221
     * Adds or replaces the specified method of the prototype.
222
     *
223
     * @param string $method The name of the method.
224
     *
225
     * @param callable $value
226
     */
227
    #[ReturnTypeWillChange]
228
    public function offsetSet($method, $value)
229
    {
230
        self::$prototypes[$this->class]->methods[$method] = $value;
231
232
        $this->revoke_consolidated_methods();
233
    }
234
235
    /**
236
     * Removed the specified method from the prototype.
237
     *
238
     * @param string $method The name of the method.
239
     */
240
    #[ReturnTypeWillChange]
241
    public function offsetUnset($method)
242
    {
243
        unset(self::$prototypes[$this->class]->methods[$method]);
244
245
        $this->revoke_consolidated_methods();
246
    }
247
248
    /**
249
     * Checks if the prototype defines the specified method.
250
     *
251
     * @param string $method The name of the method.
252
     */
253
    #[ReturnTypeWillChange]
254
    public function offsetExists($method): bool
255
    {
256
        $methods = &$this->consolidated_methods;
257
258
        if ($methods === null) {
259
            $methods = $this->consolidate_methods();
260
        }
261
262
        return isset($methods[$method]);
263
    }
264
265
    /**
266
     * Returns the callback associated with the specified method.
267
     *
268
     * @param string $method The name of the method.
269
     *
270
     * @return callable
271
     *
272
     *
273
     * @throws MethodNotDefined if the method is not defined.
274
     *
275
     * @return mixed
276
     */
277
    #[ReturnTypeWillChange]
278
    public function offsetGet($method)
279
    {
280
        $methods = &$this->consolidated_methods;
281
282
        if ($methods === null) {
283
            $methods = $this->consolidate_methods();
284
        }
285
286
        if (!isset($methods[$method])) {
287
            throw new MethodNotDefined($method, $this->class);
288
        }
289
290
        return $methods[$method];
291
    }
292
293
    /**
294
     * Returns an iterator for the prototype methods.
295
     */
296
    public function getIterator(): Traversable
297
    {
298
        $methods = &$this->consolidated_methods;
299
300
        if ($methods === null) {
301
            $methods = $this->consolidate_methods();
302
        }
303
304
        return new ArrayIterator($methods);
305
    }
306
}
307