1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of the Ariadne Component Library. |
4
|
|
|
* |
5
|
|
|
* (c) Muze <[email protected]> |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
namespace arc; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Methods to create, extend and observe prototype objects in PHP. Also adds a memoize function, |
14
|
|
|
* which is useful when using a prototype object as a Dependency Injection container. |
15
|
|
|
* @package arc |
16
|
|
|
*/ |
17
|
|
|
final class prototype |
18
|
|
|
{ |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var \SplObjectStorage contains a list of frozen objects and the observer |
22
|
|
|
*/ |
23
|
|
|
private static $frozen = null; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var \SplObjectStorage contains a list of frozen objects and the observer |
27
|
|
|
*/ |
28
|
|
|
private static $sealed = null; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var \SplObjectStorage contains a list of objects made unextensible and the observer |
32
|
|
|
*/ |
33
|
|
|
private static $notExtensible = null; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var \SplObjectStorage contains a list of all 'child' instances for each prototype |
37
|
|
|
*/ |
38
|
|
|
private static $instances = null; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var \SplObjectStorage contains a list of all observers for each prototype |
42
|
|
|
*/ |
43
|
|
|
private static $observers = null; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Returns a new \arc\prototype\Prototype object with the given properties. The |
47
|
|
|
* properties array may contain closures, these will be available as methods on |
48
|
|
|
* the new Prototype object. |
49
|
|
|
* @param array $properties List of properties and methods |
50
|
|
|
* @return \arc\prototype\Prototype |
51
|
|
|
*/ |
52
|
32 |
|
public static function create(array $properties = []) :prototype\Prototype |
53
|
|
|
{ |
54
|
32 |
|
return new prototype\Prototype($properties); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Returns a new \arc\prototype\Prototype object with the given object as its |
59
|
|
|
* prototype and the given properties and methods set. |
60
|
|
|
* @param \arc\prototype\Prototype $prototype The prototype for this object |
61
|
|
|
* @param array $properties List of properties and methods |
62
|
|
|
* @return \arc\prototype\Prototype |
63
|
|
|
*/ |
64
|
10 |
|
public static function extend(prototype\Prototype $prototype, array $properties = []) :prototype\Prototype |
65
|
|
|
{ |
66
|
10 |
|
if (!isset(self::$instances)) { |
67
|
2 |
|
self::$instances = new \SplObjectStorage(); |
68
|
|
|
}; |
69
|
10 |
|
if (!isset(self::$instances[$prototype])) { |
70
|
10 |
|
self::$instances[$prototype] = []; |
71
|
|
|
} |
72
|
10 |
|
$properties['prototype'] = $prototype; |
73
|
10 |
|
$instance = new prototype\Prototype($properties); |
74
|
10 |
|
$list = self::$instances[$prototype]; |
75
|
10 |
|
array_push($list,$instance); |
76
|
10 |
|
self::$instances[$prototype] = $list; |
77
|
10 |
|
return $instance; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Helper method to remove cache information when a prototype is no longer needed. |
82
|
|
|
* @param \arc\prototype\Prototype $obj The object to be removed |
83
|
|
|
*/ |
84
|
12 |
|
public static function _destroy(prototype\Prototype $obj) :void |
85
|
|
|
{ |
86
|
12 |
|
unset(self::$notExtensible[$obj]); |
87
|
12 |
|
unset(self::$sealed[$obj]); |
88
|
12 |
|
unset(self::$frozen[$obj]); |
89
|
12 |
|
unset(self::$observers[$obj]); |
90
|
12 |
|
if ( isset($obj->prototype) ) { |
91
|
|
|
$p = self::$instances[$obj->prototype]; |
92
|
|
|
$p = array_filter($p, function($o) use ($obj) { |
93
|
|
|
return $o != $obj; // remove references to $obj |
94
|
|
|
}); |
95
|
|
|
self::$instances[$obj->prototype] = $p; |
96
|
|
|
} |
97
|
12 |
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Returns a new \arc\prototype\Prototype with the given prototype set. In addition |
101
|
|
|
* all properties on the extra objects passed to this method will be copied to the |
102
|
|
|
* new Prototype object. For any property that is set on multiple objects, the value |
103
|
|
|
* of the property in the later object overwrites values from other objects. |
104
|
|
|
* @param \arc\prototype\Prototype $prototype the prototype for the new object |
105
|
|
|
* @param \arc\prototype\Prototype ...$object the objects whose properties will be assigned |
106
|
|
|
*/ |
107
|
2 |
|
public static function assign(prototype\Prototype $prototype, prototype\Prototype ...$objects) :prototype\Prototype |
108
|
|
|
{ |
109
|
2 |
|
$properties = []; |
110
|
2 |
|
foreach ($objects as $obj) { |
111
|
2 |
|
$properties = $obj->properties + $properties; |
112
|
|
|
} |
113
|
2 |
|
return self::extend($prototype, $properties); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* This makes changes to the given Prototype object impossible. |
118
|
|
|
* The object becomes immutable. Any attempt to change the object will silently fail. |
119
|
|
|
* @param \arc\prototype\Prototype $prototype the object to freeze |
120
|
|
|
*/ |
121
|
2 |
|
public static function freeze(prototype\Prototype $prototype) :void |
122
|
|
|
{ |
123
|
2 |
|
if (!isset(self::$frozen)) { |
124
|
2 |
|
self::$frozen = new \SplObjectStorage(); |
125
|
|
|
} |
126
|
2 |
|
self::seal($prototype); |
127
|
2 |
|
self::$frozen[$prototype] = true; |
128
|
2 |
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* This prevents reconfiguring an object or adding new properties. |
132
|
|
|
* @param \arc\prototype\Prototype $prototype the object to freeze |
133
|
|
|
*/ |
134
|
2 |
|
public static function seal(prototype\Prototype $prototype) :void |
135
|
|
|
{ |
136
|
2 |
|
if (!isset(self::$sealed)) { |
137
|
2 |
|
self::$sealed = new \SplObjectStorage(); |
138
|
|
|
} |
139
|
2 |
|
self::preventExtensions($prototype); |
140
|
2 |
|
self::$sealed[$prototype] = true; |
141
|
2 |
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Returns a list of keys of all the properties in the given prototype |
145
|
|
|
* @param \arc\prototype\Prototype $prototype |
146
|
|
|
* @return array |
147
|
|
|
*/ |
148
|
|
|
public static function keys(prototype\Prototype $prototype) :array |
149
|
|
|
{ |
150
|
|
|
$entries = static::entries($prototype); |
151
|
|
|
return array_keys($entries); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Returns an array with key:value pairs for all properties in the given prototype |
156
|
|
|
* @param \arc\prototype\Prototype $prototype |
157
|
|
|
* @return array |
158
|
|
|
*/ |
159
|
|
|
public static function entries(prototype\Prototype $prototype) :array |
160
|
|
|
{ |
161
|
|
|
return $prototype->properties; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Returns a list of all the property values in the given prototype |
166
|
|
|
* @param \arc\prototype\Prototype $prototype |
167
|
|
|
* @return array |
168
|
|
|
*/ |
169
|
|
|
public static function values(prototype\Prototype $prototype) :array |
170
|
|
|
{ |
171
|
|
|
$entries = static::entries($prototype); |
172
|
|
|
return array_values($entries); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Returns true if the the property name is available in this prototype |
177
|
|
|
* @param \arc\prototype\Prototype $prototype |
178
|
|
|
* @param string $property |
179
|
|
|
* @return bool |
180
|
|
|
*/ |
181
|
|
|
public static function hasProperty(prototype\Prototype $prototype, string $property) :bool |
182
|
|
|
{ |
183
|
|
|
$entries = static::entries($prototype); |
184
|
|
|
return array_key_exists($property, $entries); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Returns a list of all the property names defined in this prototype instance |
189
|
|
|
* without traversing its prototypes. |
190
|
|
|
* @param \arc\prototype\Prototype $prototype |
191
|
|
|
* @return array |
192
|
|
|
*/ |
193
|
|
|
public static function ownKeys(prototype\Prototype $prototype) :array |
194
|
|
|
{ |
195
|
|
|
$entries = static::ownEntries($prototype); |
196
|
|
|
return array_keys($entries); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Returns an array with key:value pairs for all properties in this prototype |
201
|
|
|
* instance wihtout traversing its prototypes. |
202
|
|
|
* @param \arc\prototype\Prototype $prototype |
203
|
|
|
* @return array |
204
|
|
|
*/ |
205
|
2 |
|
public static function ownEntries(prototype\Prototype $prototype) :array |
206
|
|
|
{ |
207
|
2 |
|
return \arc\_getOwnEntries($prototype); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Returns a list of all the property values in the given prototype |
212
|
|
|
* instance wihtout traversing its prototypes. |
213
|
|
|
* @param \arc\prototype\Prototype $prototype |
214
|
|
|
* @return array |
215
|
|
|
*/ |
216
|
|
|
public static function ownValues(prototype\Prototype $prototype) :array |
217
|
|
|
{ |
218
|
|
|
$entries = static::ownEntries($prototype); |
219
|
|
|
return array_values($entries); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Returns true if the the property name is available in this prototype |
224
|
|
|
* instance wihtout traversing its prototypes. |
225
|
|
|
* @param \arc\prototype\Prototype $prototype |
226
|
|
|
* @param string $property |
227
|
|
|
* @return bool |
228
|
|
|
*/ |
229
|
2 |
|
public static function hasOwnProperty(prototype\Prototype $prototype, string $property) :bool |
230
|
|
|
{ |
231
|
2 |
|
$entries = static::ownEntries($prototype); |
232
|
2 |
|
return array_key_exists($property, $entries); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Returns true if the given prototype is made immutable by freeze() |
237
|
|
|
* @param \arc\prototype\Prototype $prototype |
238
|
|
|
* @return bool |
239
|
|
|
*/ |
240
|
|
|
public static function isFrozen(prototype\Prototype $prototype) :bool |
241
|
|
|
{ |
242
|
|
|
return isset(self::$frozen[$prototype]); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Returns true if the given prototype is sealed by seal() |
247
|
|
|
* @param \arc\prototype\Prototype $prototype |
248
|
|
|
* @return bool |
249
|
|
|
*/ |
250
|
8 |
|
public static function isSealed(prototype\Prototype $prototype) :bool |
251
|
|
|
{ |
252
|
8 |
|
return isset(self::$sealed[$prototype]); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Returns true if the given prototype is made not Extensible |
257
|
|
|
* @param \arc\prototype\Prototype $prototype |
258
|
|
|
* @return bool |
259
|
|
|
*/ |
260
|
6 |
|
public static function isExtensible(prototype\Prototype $prototype) :bool |
261
|
|
|
{ |
262
|
6 |
|
return !isset(self::$notExtensible[$prototype]); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* This calls the $callback function each time a property of $prototype is |
267
|
|
|
* changed or unset. The callback is called with the prototype object, the |
268
|
|
|
* name of the property and the new value (null if unset). |
269
|
|
|
* If the closure returns false exactly (no other 'falsy' values will work), |
270
|
|
|
* the change will be cancelled |
271
|
|
|
* @param \arc\prototype\Prototype $prototype |
272
|
|
|
* @param \Closure $callback |
273
|
|
|
* @param array $acceptList (optional) |
274
|
|
|
*/ |
275
|
2 |
|
public static function observe(prototype\Prototype $prototype, callable $callback, array $acceptList=null) :void |
276
|
|
|
{ |
277
|
2 |
|
if ( !isset(self::$observers) ) { |
278
|
2 |
|
self::$observers = new \SplObjectStorage(); |
279
|
|
|
} |
280
|
2 |
|
if ( !isset(self::$observers[$prototype]) ) { |
281
|
2 |
|
self::$observers[$prototype] = []; |
282
|
|
|
} |
283
|
2 |
|
if ( !isset($acceptList) ) { |
284
|
2 |
|
$acceptList = ['add','update','delete','reconfigure']; |
285
|
|
|
} |
286
|
2 |
|
$observers = self::$observers[$prototype]; |
287
|
2 |
|
foreach( $acceptList as $acceptType ) { |
288
|
2 |
|
if ( !isset($observers[$acceptType]) ) { |
289
|
2 |
|
$observers[$acceptType] = new \SplObjectStorage(); |
290
|
|
|
} |
291
|
2 |
|
$observers[$acceptType][$callback] = true; |
292
|
|
|
} |
293
|
2 |
|
self::$observers[$prototype] = $observers; |
294
|
2 |
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Returns a list of observers for the given prototype. |
298
|
|
|
* @param \arc\prototype\Prototype $prototype |
299
|
|
|
* @return array |
300
|
|
|
*/ |
301
|
2 |
|
public static function getObservers(prototype\Prototype $prototype) :array |
302
|
|
|
{ |
303
|
2 |
|
return (isset(self::$observers[$prototype]) ? self::$observers[$prototype] : [] ); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Makes an object no longer extensible. |
308
|
|
|
* @param \arc\prototype\Prototype $prototype |
309
|
|
|
*/ |
310
|
4 |
|
public static function preventExtensions(prototype\Prototype $prototype) :void |
311
|
|
|
{ |
312
|
4 |
|
if ( !isset(self::$notExtensible) ) { |
313
|
2 |
|
self::$notExtensible = new \SplObjectStorage(); |
314
|
|
|
} |
315
|
4 |
|
self::$notExtensible[$prototype] = true; |
316
|
4 |
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Removes an observer callback for the given prototype. |
320
|
|
|
* @param \arc\prototype\Prototyp $prototype |
321
|
|
|
* @param \Closure $callback the observer callback to be removed |
322
|
|
|
*/ |
323
|
|
|
public static function unobserve(prototype\Prototype $prototype, callable $callback) :void |
324
|
|
|
{ |
325
|
|
|
if ( isset(self::$observers) && isset(self::$observers[$prototype]) ) { |
326
|
|
|
unset(self::$observers[$prototype][$callback]); |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Returns true if the object as the given prototype somewhere in its |
332
|
|
|
* prototype chain, including itself. |
333
|
|
|
* @param \arc\prototype\Prototype $object |
334
|
|
|
* @param \arc\prototype\Prototype $prototype |
335
|
|
|
* @return bool |
336
|
|
|
*/ |
337
|
2 |
|
public static function hasPrototype(prototype\Prototype $obj, prototype\Prototype $prototype) :bool |
338
|
|
|
{ |
339
|
2 |
|
if (!$obj->prototype) { |
340
|
|
|
return false; |
341
|
|
|
} |
342
|
2 |
|
if ($obj === $prototype || $obj->prototype === $prototype) { |
343
|
2 |
|
return true; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
return static::hasPrototype($obj->prototype, $prototype ); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Returns a list of prototype objects that have this prototype object |
351
|
|
|
* in their prototype chain. |
352
|
|
|
* @param \arc\prototype\Prototype $prototype |
353
|
|
|
* @return array |
354
|
|
|
*/ |
355
|
|
|
public static function getDescendants(prototype\Prototype $prototype) :array |
356
|
|
|
{ |
357
|
|
|
$instances = self::getInstances($prototype); |
358
|
|
|
$descendants = $instances; |
359
|
|
|
foreach ($instances as $instance) { |
360
|
|
|
$descendants += self::getDescendants($instance); |
361
|
|
|
} |
362
|
|
|
return $descendants; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* Returns a list of prototype objects that have this prototype object |
367
|
|
|
* as their direct prototype. |
368
|
|
|
* @param \arc\prototype\Prototype $prototype |
369
|
|
|
* @return array |
370
|
|
|
*/ |
371
|
2 |
|
public static function getInstances(prototype\Prototype $prototype) :array |
372
|
|
|
{ |
373
|
2 |
|
return (isset(self::$instances[$prototype]) ? self::$instances[$prototype] : [] ); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Returns the full prototype chain for the given object. |
378
|
|
|
* @param \arc\prototype\Prototype $obj |
379
|
|
|
* @return array |
380
|
|
|
*/ |
381
|
2 |
|
public static function getPrototypes(prototype\Prototype $obj) :array |
382
|
|
|
{ |
383
|
2 |
|
$prototypes = []; |
384
|
2 |
|
while ( $prototype = $obj->prototype ) { |
385
|
2 |
|
$prototypes[] = $prototype; |
386
|
2 |
|
$obj = $prototype; |
387
|
|
|
} |
388
|
2 |
|
return $prototypes; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Returns a new function that calls the given function just once and then simply |
393
|
|
|
* returns its result on each subsequent call. |
394
|
|
|
* @param callable function to call just once and then remember the result |
|
|
|
|
395
|
|
|
* @return \Closure |
396
|
|
|
*/ |
397
|
|
|
public static function memoize(callable $f) |
398
|
|
|
{ |
399
|
|
|
return memoize($f); |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Helper function to make sure that the returned Closure is not defined in a static scope. |
405
|
|
|
* @param callable function to call just once and then remember the result |
406
|
|
|
* @return \Closure |
407
|
|
|
*/ |
408
|
1 |
|
function memoize(callable $f) :callable |
409
|
|
|
{ |
410
|
|
|
return function () use ($f) { |
411
|
|
|
static $result; |
412
|
|
|
if (null === $result) { |
413
|
|
|
if ( $f instanceof \Closure && isset($this) ) { |
|
|
|
|
414
|
|
|
$f = \Closure::bind($f, $this); |
415
|
|
|
} |
416
|
|
|
$result = $f(); |
417
|
|
|
} |
418
|
|
|
return $result; |
419
|
|
|
}; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* 'private' function that must be declared outside static scope, so we can bind |
424
|
|
|
* the closure to an object to peek into its private _ownProperties property |
425
|
|
|
* @param \arc\prototype\Prototype $prototype |
426
|
|
|
* @return array |
427
|
|
|
*/ |
428
|
1 |
|
function _getOwnEntries(prototype\Prototype $prototype) :array |
429
|
|
|
{ |
430
|
|
|
// this needs access to the private _ownProperties variable |
431
|
|
|
// this is one way to do that. |
432
|
2 |
|
$f = \Closure::bind(function() { |
433
|
2 |
|
return $this->_ownProperties; |
|
|
|
|
434
|
2 |
|
}, $prototype, $prototype); |
435
|
2 |
|
return $f(); |
436
|
|
|
} |
437
|
|
|
|
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths