Prototype   F
last analyzed

Complexity

Total Complexity 102

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Test Coverage

Coverage 84.78%

Importance

Changes 6
Bugs 0 Features 1
Metric Value
eloc 184
c 6
b 0
f 1
dl 0
loc 422
ccs 156
cts 184
cp 0.8478
rs 2
wmc 102

22 Methods

Rating   Name   Duplication   Size   Complexity  
A _tryToCall() 0 7 4
A _getLocalProperties() 0 3 1
A __destruct() 0 4 1
B _isGetterOrSetter() 0 9 10
A __clone() 0 9 4
A __invoke() 0 6 2
A _isBindableSetter() 0 4 3
A _getPrototypeProperty() 0 21 6
A _bind() 0 8 2
C __get() 0 27 13
A jsonSerialize() 0 6 2
A __unset() 0 26 5
B __call() 0 17 7
A __construct() 0 12 6
A _getPublicProperties() 0 8 2
B __set() 0 51 9
B _isReadOnly() 0 9 9
A __toString() 0 3 1
A _purgePrototypeCache() 0 9 3
B _isAccessibleGetterOrSetter() 0 22 7
A _isNonBindableSetter() 0 4 3
A __isset() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Prototype often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Prototype, and based on these observations, apply Extract Interface, too.

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\prototype;
11
12
/**
13
 * Implements a class of objects with prototypical inheritance, getters/setters, and observable changes
14
 * very similar to EcmaScript objects
15
 * @property \arc\prototype\Prototype $prototype The prototype for this object
16
 * @property array $properties
17
 */
18
final class Prototype implements \JsonSerializable
19
{
20
    /**
21
     * @var array cache for prototype properties
22
     */
23
    private static $properties = [];
24
25
    /**
26
     * @var array store for all properties of this instance. Must be private to always trigger __set and observers
27
     */
28
    private $_ownProperties = [];
29
30
    /**
31
     * @var array contains a list of local methods that have a static scope, such methods must be prefixed with a ':' when defined.
32
     */
33
    private $_staticMethods = [];
34
35
    /**
36
    * @var Prototype prototype Readonly reference to a prototype object. Can only be set in the constructor.
37
    */
38
    private $prototype = null;
39
40
    /**
41
     * @param array $properties
42
     */
43 32
    public function __construct($properties = [])
44
    {
45 32
        foreach ($properties as $property => $value) {
46 28
            if ( !is_numeric( $property ) && $property!='properties' ) {
47 28
                 if ( $property[0] == ':' ) {
48 4
                    $property = substr($property, 1);
49 4
                    $this->_staticMethods[$property] = true;
50 4
                    $this->_ownProperties[$property] = $value;
51 28
                } else if ($property == 'prototype') {
52 10
                    $this->prototype = $value;
53
                } else {
54 28
                    $this->_ownProperties[$property] = $this->_bind( $value );
55
                }
56
            }
57
        }
58 32
    }
59
60
    /**
61
     * @param $name
62
     * @param $args
63
     * @return mixed
64
     * @throws \BadMethodCallException
65
     */
66 10
    public function __call($name, $args)
67
    {
68 10
        if (array_key_exists( $name, $this->_ownProperties ) && is_callable( $this->_ownProperties[$name] )) {
69 10
            if ( array_key_exists($name, $this->_staticMethods) ) {
70 2
                array_unshift($args, $this);
71
            }
72 10
            return call_user_func_array( $this->_ownProperties[$name], $args );
73 4
        } elseif (is_object( $this->prototype)) {
74 4
            $method = $this->_getPrototypeProperty( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $method is correct as $this->_getPrototypeProperty($name) targeting arc\prototype\Prototype::_getPrototypeProperty() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
75 4
            if (is_callable( $method )) {
76 4
                if ( array_key_exists($name, $this->_staticMethods) ) {
77
                    array_unshift($args, $this);
78
                }
79 4
                return call_user_func_array( $method, $args );
0 ignored issues
show
Bug introduced by
$method of type null is incompatible with the type callable expected by parameter $callback of call_user_func_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
                return call_user_func_array( /** @scrutinizer ignore-type */ $method, $args );
Loading history...
80
            }
81
        }
82
        throw new \BadMethodCallException( $name.' is not a method on this Object');
83
    }
84
85
    /**
86
     * @param $name
87
     * @return array|null|Prototype
88
     */
89 24
    public function __get($name)
90
    {
91 24
        switch ($name) {
92 24
            case 'prototype':
93 2
                return $this->prototype;
94
            break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
95 22
            case 'properties':
96 2
                return $this->_getPublicProperties();
97
            break;
98
            default:
99 22
                if ( array_key_exists($name, $this->_ownProperties) ) {
100 22
                    $property = $this->_ownProperties[$name];
101 22
                    if ( is_array($property) ) {
102 6
                        if ( isset($property['get']) && is_callable($property['get']) ) {
103 4
                            $getter = \Closure::bind( $property['get'], $this, $this );
104 4
                            return $getter();
105 2
                        } else if ( isset($property[':get']) && is_callable($property[':get']) ) {
106 2
                            return $property[':get']($this);
107
                        } else if ( (isset($property['set']) && is_callable($property['set']) )
108
                            || ( isset($property[':set']) && is_callable($property[':set']) ) ) {
109
                            return null;
110
                        }
111
                    }
112 16
                    return $property;
113
                }
114 2
                return $this->_getPrototypeProperty( $name );
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->_getPrototypeProperty($name) targeting arc\prototype\Prototype::_getPrototypeProperty() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
115
            break;
116
        }
117
    }
118
119 8
    private function _isGetterOrSetter($property) {
120
        return (
121 8
            isset($property)
122 8
            && is_array($property)
123
            && (
124 6
                 ( isset($property['get']) && is_callable($property['get']) )
125 2
                || ( isset($property[':get']) && is_callable($property[':get']) )
126
                || ( isset($property['set']) && is_callable($property['set']) )
127 8
                || ( isset($property[':set']) && is_callable($property[':set']) )
128
            )
129
        );
130
    }
131
132
    /**
133
     * @param $name
134
     * @param $value
135
     * @returns Boolean
136
     * @throws \LogicException
137
     */
138 12
    private function _isAccessibleGetterOrSetter($name, $value) {
139 12
        if (in_array( $name, [ 'prototype', 'properties' ] )) {
140
            throw new \LogicException('Property "'.$name.'" is read only.');
141
        }
142
143 12
        if ( !isset($this->_ownProperties[$name]) && !\arc\prototype::isExtensible($this) ) {
144 4
            throw new \LogicException('Object is not extensible.');
145
        }
146
147 8
        $valueIsSetterOrGetter = $this->_isGetterOrSetter($value);
148
149 8
        if (isset($this->_ownProperties[$name])) {
150 8
            $propertyIsSetterOrGetter = $this->_isGetterOrSetter($this->_ownProperties[$name]);
151
        } else {
152 2
            $propertyIsSetterOrGetter = false;
153
        }
154
        
155 8
        if ( \arc\prototype::isSealed($this) && $valueIsSetterOrGetter!=$propertyIsSetterOrGetter ) {
156
            throw new \LogicException('Object is sealed.');
157
        }
158
        
159 8
        return $valueIsSetterOrGetter;
160
    }
161
162 8
    private function _isBindableSetter($property) {
163 8
        return $this->_isGetterOrSetter($property)
164 8
            && isset($property['set']) 
165 8
            && is_callable($property['set']);
166
    }
167
168 6
    private function _isNonBindableSetter($property) {
169 6
        return $this->_isGetterOrSetter($property)
170 6
            && isset($property[':set']) 
171 6
            && is_callable($property[':set']);
172
    }
173
174 4
    private function _isReadOnly($property) {
175 4
        return $this->_isGetterOrSetter($property)
176
            && ( 
177 2
                (isset($property['get']) && is_callable($property['get']) )
178 4
                || (isset($property[':get']) && is_callable($property[':get']) )
179
            )
180
            && !( 
181 2
                (isset($property['set']) && is_callable($property['set']) )
182 4
                || (isset($property[':set']) && is_callable($property[':set']) )
183
            );
184
    }
185
186 2
    private function _purgePrototypeCache($name, $changes) {
187
        // purge prototype cache for this property - this will clear too much but cache will be filled again
188
        // clearing exactly the right entries from the cache will generally cost more performance than this
189 2
        unset( self::$properties[ $name ] );
190 2
        $observers = \arc\prototype::getObservers($this);
191
192 2
        if ( isset($observers[$changes['type']]) ) {
193 2
            foreach($observers[$changes['type']] as $observer) {
194 2
                $observer($changes);
195
            }
196
        }
197 2
    }
198
    
199
    /**
200
     * @param $name
201
     * @param $value
202
     * @throws \LogicException
203
     */
204 12
    public function __set($name, $value)
205
    {
206 12
        $valueIsSetterOrGetter = $this->_isAccessibleGetterOrSetter($name, $value);
207
208
        $changes = [
209 8
            'name'   => $name,
210 8
            'object' => $this
211
        ];
212
        
213 8
        if ( array_key_exists($name, $this->_ownProperties) ) {
214 8
            $changes['type']     = 'update';
215 8
            $changes['oldValue'] = $this->_ownProperties[$name];
216
        } else {
217 2
            $changes['type']     = 'add';
218
        }
219
220 8
        $clearcache = false;
221
        // get current value for $name, to check if it has a getter and/or a setter
222 8
        if ( array_key_exists($name, $this->_ownProperties) ) {
223 8
            $current = $this->_ownProperties[$name];
224
        } else {
225 2
            $current = $this->_getPrototypeProperty($name);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $current is correct as $this->_getPrototypeProperty($name) targeting arc\prototype\Prototype::_getPrototypeProperty() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
226
        }
227
228 8
        if ( $valueIsSetterOrGetter ) {
229
            // reconfigure current property
230
            $clearcache = true;
231
            $this->_ownProperties[$name] = $value;
232
            unset($this->_staticMethods[$name]);
233 8
        } else if ( $this->_isBindableSetter($current) ) {
234
            // bindable setter found, use it, no need to set anything in _ownProperties
235 2
            $setter = \Closure::bind($current['set'], $this, $this);
236 2
            $setter($value);
237 6
        } else if ( $this->_isNonBindableSetter($current) ) {
238
            // nonbindable setter found
239 2
            $current[':set']($this, $value);
240 4
        } else if ( $this->_isReadOnly($current) ) {
241
            // there is only a getter, no setter, so ignore setting this property, its readonly.
242 2
            throw new \LogicException('Property "'.$name.'" is readonly.');
243 2
        } else if (!array_key_exists($name, $this->_staticMethods)) {
244
            // bindable value, update _ownProperties, so clearcache as well
245 2
            $clearcache = true;
246 2
            $this->_ownProperties[$name] = $this->_bind( $value );
247
        } else {
248
            // non bindable value, update _ownProperties, so clearcache as well
249
            $clearcache = true;
250
            $this->_ownProperties[$name] = $value;
251
        }
252
253 6
        if ( $clearcache ) {
254 2
            $this->_purgePrototypeCache($name, $changes);
255
        }
256
257 6
    }
258
259
    /**
260
     * Returns a list of publically accessible properties of this object and its prototypes.
261
     * @return array
262
     */
263 2
    private function _getPublicProperties()
264
    {
265
        // get public properties only, so use closure to escape local scope.
266
        // the anonymous function / closure is needed to make sure that get_object_vars
267
        // only returns public properties.
268 2
        return ( is_object( $this->prototype )
269 2
            ? array_merge( $this->prototype->properties, $this->_getLocalProperties() )
270 2
            : $this->_getLocalProperties() );
271
    }
272
273
    /**
274
     * Returns a list of publically accessible properties of this object only, disregarding its prototypes.
275
     * @return array
276
     */
277 2
    private function _getLocalProperties()
278
    {
279 2
        return [ 'prototype' => $this->prototype ] + $this->_ownProperties;
280
    }
281
282
    /**
283
     * Get a property from the prototype chain and caches it.
284
     * @param $name
285
     * @return null
286
     */
287 18
    private function _getPrototypeProperty($name)
288
    {
289 18
        if (is_object( $this->prototype )) {
290
            // cache prototype access per property - allows fast but partial cache purging
291 6
            if (!array_key_exists( $name, self::$properties )) {
292 6
                self::$properties[ $name ] = new \SplObjectStorage();
293
            }
294 6
            if (!self::$properties[$name]->contains( $this->prototype )) {
295 6
                $property = $this->prototype->{$name};
296 6
                if ( $property instanceof \Closure ) {
297 4
                    if ( !array_key_exists($name, $this->prototype->_staticMethods)) {
298 4
                        $property = $this->_bind( $property );
299
                    } else {
300
                        $this->_staticMethods[$name] = true;
301
                    }
302
                }
303 6
                self::$properties[$name][ $this->prototype ] = $property;
304
            }
305 6
            return self::$properties[$name][ $this->prototype ];
306
        } else {
307 14
            return null;
308
        }
309
    }
310
311
312
    /**
313
     * @param $name
314
     * @return bool
315
     */
316 14
    public function __isset($name)
317
    {
318 14
        if ( array_key_exists($name, $this->_ownProperties) ) {
319 4
            return isset($this->_ownProperties[$name]);
320
        } else {
321 12
            $val = $this->_getPrototypeProperty( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $val is correct as $this->_getPrototypeProperty($name) targeting arc\prototype\Prototype::_getPrototypeProperty() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
322 12
            return isset( $val );
323
        }
324
    }
325
326
    /**
327
     * @param $name
328
     * @throws \LogicException
329
     */
330 2
    public function __unset($name) {
331 2
        if (!in_array( $name, [ 'prototype', 'properties' ] )) {
332 2
            if ( !\arc\prototype::isSealed($this) ) {
333 2
                $oldValue = $this->_ownProperties[$name];
334 2
                if (array_key_exists($name, $this->_staticMethods)) {
335
                    unset($this->_staticMethods[$name]);
336
                }
337 2
                unset($this->_ownProperties[$name]);
338
                // purge prototype cache for this property - this will clear too much but cache will be filled again
339
                // clearing exactly the right entries from the cache will generally cost more performance than this
340 2
                unset( self::$properties[ $name ] );
341 2
                $observers = \arc\prototype::getObservers($this);
342
                $changes = [
343 2
                    'type' => 'delete',
344 2
                    'name' => $name,
345 2
                    'object' => $this,
346 2
                    'oldValue' => $oldValue
347
                ];
348 2
                foreach ($observers['delete'] as $observer) {
349 2
                    $observer($changes);
350
                }
351
            } else {
352 2
                throw new \LogicException('Object is sealed.');
353
            }
354
        } else {
355
            throw new \LogicException('Property "'.$name.'" is protected.');
356
        }
357 2
    }
358
359
    /**
360
     *
361
     */
362 12
    public function __destruct()
363
    {
364 12
    	\arc\prototype::_destroy($this);
365 12
        return $this->_tryToCall( '__destruct' );
366
    }
367
368
    /**
369
     * @return mixed
370
     */
371 4
    public function __toString()
372
    {
373 4
        return (string) $this->_tryToCall( '__toString' );
374
    }
375
376
    /**
377
     * @return mixed
378
     * @throws \BadMethodCallException
379
     */
380 2
    public function __invoke()
381
    {
382 2
        if (is_callable( $this->__invoke )) {
0 ignored issues
show
Bug Best Practice introduced by
The property __invoke does not exist on arc\prototype\Prototype. Since you implemented __get, consider adding a @property annotation.
Loading history...
383 2
            return call_user_func_array( $this->__invoke, func_get_args() );
0 ignored issues
show
Bug introduced by
It seems like $this->__invoke can also be of type null; however, parameter $callback of call_user_func_array() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

383
            return call_user_func_array( /** @scrutinizer ignore-type */ $this->__invoke, func_get_args() );
Loading history...
384
        } else {
385
            throw new \BadMethodCallException( 'No __invoke method found in this Object' );
386
        }
387
    }
388
389
    /**
390
     *
391
     */
392
    public function __clone()
393
    {
394
        // make sure all methods are bound to $this - the new clone.
395
        foreach ($this->_ownProperties as $name => $property) {
396
            if ( $property instanceof \Closure && !$this->_staticMethods[$name] ) {
397
                $this->{$name} = $this->_bind( $property );
398
            }
399
        }
400
        $this->_tryToCall( '__clone' );
401
    }
402
403
    public function jsonSerialize() {
404
        $result = $this->_tryToCall( '__toJSON' );
405
        if (!$result) {
406
            return \arc\prototype::entries($this);
407
        } else {
408
            return $result;
409
        }
410
    }
411
412
    /**
413
     * Binds the property to this object
414
     * @param $property
415
     * @return mixed
416
     */
417 30
    private function _bind($property)
418
    {
419 30
        if ($property instanceof \Closure ) {
420
            // make sure any internal $this references point to this object and not the prototype or undefined
421 14
            return \Closure::bind( $property, $this );
422
        }
423
424 22
        return $property;
425
    }
426
427
    /**
428
     * Only call $f if it is a callable.
429
     * @param $f
430
     * @param array $args
431
     * @return mixed
432
     */
433 14
    private function _tryToCall($name, $args = [])
434
    {
435 14
        if ( isset($this->{$name}) && is_callable( $this->{$name} )) {
436 4
            if ( array_key_exists($name, $this->_staticMethods) ) {
437 2
                array_unshift($args, $this);
438
            }
439 4
            return call_user_func_array( $this->{$name}, $args );
440
        }
441 12
    }
442
}
443
444
/**
445
 * Class dummy
446
 * This class is needed because in PHP7 you can no longer bind to \stdClass
447
 * And anonymous classes are syntax errors in PHP5.6, so there.
448
 * @package arc\lambda
449
 */
450
class dummy {
451
}
452