Completed
Push — master ( d03b1e...caf135 )
by Auke
62:24 queued 10s
created

prototype.php ➔ memoize()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 1
nop 1
dl 0
loc 13
ccs 4
cts 4
cp 1
crap 4
rs 9.8333
c 0
b 0
f 0
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Bug introduced by
There is no parameter named $object. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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