Completed
Push — master ( 060a1c...20a26b )
by Auke
13:10 queued 38s
created

prototype::ownValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 2
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\Object 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\Object
51
     */
52 14
    public static function create($properties) 
53
    {
54 14
        return new prototype\Object($properties);
55
    }
56
57
    /**
58
     * Returns a new \arc\prototype\Object object with the given object as its
59
     * prototype and the given properties and methods set.
60
     * @param \arc\prototype\Object $prototype The prototype for this object
61
     * @param array $properties List of properties and methods
62
     * @return \arc\prototype\Object
63
     */
64 5
    public static function extend($prototype, $properties) 
65
    {
66 5
        if ( self::isExtensible($prototype) ) {
67 4
            if (!isset(self::$instances)) {
68 1
                self::$instances = new \SplObjectStorage();
69
            };
70 4
            if (!isset(self::$instances[$prototype])) {
71 4
                self::$instances[$prototype] = [];
72
            }
73 4
            $properties['prototype'] = $prototype;
74 4
            $instance = new prototype\Object($properties);
75 4
            $list = self::$instances[$prototype];
76 4
            array_push($list,$instance);
77 4
            self::$instances[$prototype] = $list;
78 4
            return $instance;
79
        } else {
80 1
            return null;
81
        }
82
    }
83
84
    /**
85
     * Helper method to remove cache information when a prototype is no longer needed.
86
     * @param \arc\prototype\Object $obj The object to be removed
87
     */
88 6
    public static function _destroy($obj) 
89
    {
90 6
        unset(self::$notExtensible[$obj]);
91 6
        unset(self::$sealed[$obj]);
92 6
        unset(self::$frozen[$obj]);
93 6
        unset(self::$observers[$obj]);
94 6
        if ( isset($obj->prototype) ) {
95
            unset(self::$instances[$obj->prototype][$obj]);
96
        }
97 6
    }
98
99
    /**
100
     * Returns a new \arc\prototype\Object 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\Object $prototype the prototype for the new object
105
     * @param \arc\prototype\Object ...$object the objects whose properties will be assigned
106
     */
107 1
    public static function assign($prototype) 
108
    {
109 1
        $objects = func_get_args();
110 1
        array_shift($objects);
111 1
        $properties = [];
112 1
        foreach ($objects as $obj) {
113 1
            $properties = $obj->properties + $properties;
114
        }
115 1
        return self::extend($prototype, $properties);
116
    }
117
118
    /**
119
     * This makes changes to the given Prototype object impossible. 
120
     * The object becomes immutable. Any attempt to change the object will silently fail.
121
     * @param \arc\prototype\Object $prototype the object to freeze
122
     */
123 1
    public static function freeze($prototype) 
124
    {
125 1
        if (!isset(self::$frozen)) {
126 1
            self::$frozen = new \SplObjectStorage();
127
        }
128 1
        self::seal($prototype);
129 1
        self::$frozen[$prototype] = true;
130 1
    }
131
132
    /**
133
     * This prevents reconfiguring an object or adding new properties.
134
     * @param \arc\prototype\Object $prototype the object to freeze
135
     */
136 1
    public static function seal($prototype) 
137
    {
138 1
        if (!isset(self::$sealed)) {
139 1
            self::$sealed = new \SplObjectStorage();
140
        }
141 1
        self::preventExtensions($prototype);
142 1
        self::$sealed[$prototype] = true;
143 1
    }
144
145
    /**
146
     * Returns a list of keys of all the properties in the given prototype
147
     * @param \arc\prototype\Object $prototype
148
     * @return array
149
     */
150
    public static function keys($prototype) 
151
    {
152
        $entries = static::entries($prototype);
153
        return array_keys($entries);
154
    }
155
156
    /**
157
     * Returns an array with key:value pairs for all properties in the given prototype
158
     * @param \arc\prototype\Object $prototype
159
     * @return array
160
     */
161 1
    public static function entries($prototype) 
162
    {
163 1
        return $prototype->properties;
164
    }
165
166
    /**
167
     * Returns a list of all the property values in the given prototype
168
     * @param \arc\prototype\Object $prototype
169
     * @return array
170
     */
171
    public static function values($prototype) 
172
    {
173
        $entries = static::entries($prototype);
174
        return array_values($entries);
175
    }
176
177
    /**
178
     * Returns true if the the property name is available in this prototype
179
     * @param \arc\prototype\Object $prototype
180
     * @param string $property
181
     * @return bool
182
     */
183
    public static function hasProperty($prototype, $property) 
184
    {
185
        $entries = static::entries($prototype);
186
        return array_key_exists($property, $entries);
187
    }
188
189
    /**
190
     * Returns a list of all the property names defined in this prototype instance
191
     * without traversing its prototypes.
192
     * @param \arc\prototype\Object $prototype
193
     * @return array
194
     */
195
    public static function ownKeys($prototype) 
196
    {
197
        $entries = static::ownEntries($prototype);
198
        return array_keys($entries);
199
    }
200
201
    /**
202
     * Returns an array with key:value pairs for all properties in this prototype
203
     * instance wihtout traversing its prototypes.
204
     * @param \arc\prototype\Object $prototype
205
     * @return array
206
     */
207 1
    public static function ownEntries($prototype) 
208
    {
209 1
        return \arc\_getOwnEntries($prototype);
210
    }
211
212
    /**
213
     * Returns a list of all the property values in the given prototype
214
     * instance wihtout traversing its prototypes.
215
     * @param \arc\prototype\Object $prototype
216
     * @return array
217
     */
218
    public static function ownValues($prototype) 
219
    {
220
        $entries = static::ownEntries($prototype);
221
        return array_values($entries);
222
    }
223
224
    /**
225
     * Returns true if the the property name is available in this prototype
226
     * instance wihtout traversing its prototypes.
227
     * @param \arc\prototype\Object $prototype
228
     * @param string $property
229
     * @return bool
230
     */
231 1
    public static function hasOwnProperty($prototype, $property) 
232
    {
233 1
        $entries = static::ownEntries($prototype);
234 1
        return array_key_exists($property, $entries);
235
    }
236
237
    /**
238
     * Returns true if the given prototype is made immutable by freeze()
239
     * @param \arc\prototype\Object $prototype
240
     * @return bool
241
     */
242
    public static function isFrozen($prototype) 
243
    {
244
        return isset(self::$frozen[$prototype]);
245
    }
246
247
    /**
248
     * Returns true if the given prototype is sealed by seal()
249
     * @param \arc\prototype\Object $prototype
250
     * @return bool
251
     */
252 4
    public static function isSealed($prototype) 
253
    {
254 4
        return isset(self::$sealed[$prototype]);
255
    }
256
257
    /**
258
     * Returns true if the given prototype is made not Extensible
259
     * @param \arc\prototype\Object $prototype
260
     * @return bool
261
     */
262 7
    public static function isExtensible($prototype) 
263
    {
264 7
        return !isset(self::$notExtensible[$prototype]);
265
    }
266
267
    /**
268
     * This calls the $callback function each time a property of $prototype is 
269
     * changed or unset. The callback is called with the prototype object, the 
270
     * name of the property and the new value (null if unset).
271
     * If the closure returns false exactly (no other 'falsy' values will work), 
272
     * the change will be cancelled
273
     * @param \arc\prototype\Object $prototype
274
     * @param \Closure $callback
275
     */
276 1
    public static function observe($prototype, $callback, $acceptList=null) 
277
    {
278 1
        if ( !isset(self::$observers) ) {
279 1
            self::$observers = new \SplObjectStorage();
280
        }
281 1
        if ( !isset(self::$observers[$prototype]) ) {
282 1
            self::$observers[$prototype] = [];
283
        }
284 1
        if ( !isset($acceptList) ) {
285 1
            $acceptList = ['add','update','delete','reconfigure'];
286
        }
287 1
        $observers = self::$observers[$prototype];
288 1
        foreach( $acceptList as $acceptType ) {
289 1
            if ( !isset($observers[$acceptType]) ) {
290 1
                $observers[$acceptType] = new \SplObjectStorage();
291
            }
292 1
            $observers[$acceptType][$callback] = true;
293
        }
294 1
        self::$observers[$prototype] = $observers;
295 1
    }
296
297
    /**
298
     * Returns a list of observers for the given prototype.
299
     * @param \arc\prototype\Object $prototype
300
     * @return array
301
     */
302 1
    public static function getObservers($prototype) 
303
    {
304 1
        return (isset(self::$observers[$prototype]) ? self::$observers[$prototype] : [] );
305
    }
306
307
    /**
308
     * Makes an object no longer extensible.
309
     * @param \arc\prototype\Object $prototype
310
     */
311 2
    public static function preventExtensions($prototype) 
312
    {
313 2
        if ( !isset(self::$notExtensible) ) {
314 1
            self::$notExtensible = new \SplObjectStorage();
315
        }
316 2
        self::$notExtensible[$prototype] = true;
317 2
    }
318
319
    /**
320
     * Removes an observer callback for the given prototype.
321
     * @param \arc\prototype\Prototyp $prototype
322
     * @param \Closure $callback the observer callback to be removed
323
     */
324
    public static function unobserve($prototype, $callback) 
325
    {
326
        if ( isset(self::$observers) && isset(self::$observers[$prototype]) ) {
327
            unset(self::$observers[$prototype][$callback]);
328
        }
329
    }
330
331
    /**
332
     * Returns true if the object as the given prototype somewhere in its
333
     * prototype chain, including itself.
334
     */
335
    public static function hasPrototype($obj, $prototype) 
336
    {
337
        if (!$obj->prototype) {
338
            return false;
339
        }
340
        if ($obj === $prototype || $obj->prototype === $prototype) {
341
            return true;
342
        }
343
344
        return static::hasPrototype($obj->prototype, $prototype );
345
    }
346
347
    /**
348
     * Returns a list of prototype objects that have this prototype object
349
     * in their prototype chain.
350
     * @param \arc\prototype\Object $prototype
351
     * @return array
352
     */
353
    public static function getDescendants($prototype) 
354
    {
355
        $instances = self::getInstances($prototype);
356
        $descendants = $instances;
357
        foreach ($instances as $instance) {
358
            $descendants += self::getDescendants($instance);
359
        }
360
        return $descendants;
361
    }
362
363
    /**
364
     * Returns a list of prototype objects that have this prototype object
365
     * as their direct prototype.
366
     * @param \arc\prototype\Object $prototype
367
     * @return array
368
     */
369
    public static function getInstances($prototype) 
370
    {
371
        return (isset(self::$instances[$prototype]) ? self::$instances[$prototype] : [] );
372
    }
373
374
    /**
375
     * Returns the full prototype chain for the given object.
376
     * @param \arc\prototype\Object $obj
377
     * @return array
378
     */
379
    public static function getPrototypes($obj) 
380
    {
381
        $prototypes = [];
382
        while ( $prototype = $obj->prototype ) {
383
            $prototypes[] = $prototype;
384
            $obj = $prototype;
385
        }
386
        return $prototypes;
387
    }
388
389
    /**
390
     * Returns a new function that calls the given function just once and then simply
391
     * returns its result on each subsequent call.
392
     * @param callable function to call just once and then remember the result
393
     */
394
    public static function memoize($f) 
395
    {
396
        return memoize($f);
397
    }
398
}
399
400
/**
401
 * Helper function to make sure that the returned Closure is not defined in a static scope.
402
 * @param callable function to call just once and then remember the result
403
 */
404
function memoize($f) 
405
{
406
    return function () use ($f) {
407
        static $result;
408
        if (null === $result) {
409
            if ( $f instanceof \Closure && isset($this) ) {
1 ignored issue
show
Bug introduced by
The variable $this seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
410
                $f = \Closure::bind($f, $this);
1 ignored issue
show
Bug introduced by
Consider using a different name than the imported variable $f, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
411
            }
412
            $result = $f();
413
        }
414
        return $result;
415
    };
416
}
417
418
/**
419
 * 'private' function that must be declared outside static scope, so we can bind
420
 * the closure to an object to peek into its private _ownProperties property
421
 */
422
function _getOwnEntries($prototype) {
423
    // this needs access to the private _ownProperties variable
424
    // this is one way to do that.
425
    $f = \Closure::bind(function() {
426 1
        return $this->_ownProperties;
1 ignored issue
show
Bug introduced by
The variable $this does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
427 1
    }, $prototype, $prototype);
428
    return $f();
429
}