Prototype::__set()   B
last analyzed

Complexity

Conditions 9
Paths 44

Size

Total Lines 51
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 9.3752

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 33
c 3
b 0
f 0
nc 44
nop 2
dl 0
loc 51
ccs 25
cts 30
cp 0.8333
crap 9.3752
rs 8.0555

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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