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