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

Object   D

Complexity

Total Complexity 91

Size/Duplication

Total Lines 361
Duplicated Lines 3.05 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 95.71%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
wmc 91
c 3
b 1
f 1
lcom 1
cbo 2
dl 11
loc 361
ccs 134
cts 140
cp 0.9571
rs 4.8717

16 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 16 6
B __call() 0 18 7
C __get() 0 29 13
B _isGetterOrSetter() 0 12 10
C __set() 5 72 25
A _getPublicProperties() 0 9 2
A _getLocalProperties() 0 4 1
B _getPrototypeProperty() 0 23 6
A __isset() 6 9 2
B __unset() 0 24 5
A __destruct() 0 5 1
A __toString() 0 4 1
A __invoke() 0 8 2
A __clone() 0 10 4
A _bind() 0 9 2
A _tryToCall() 0 9 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Object 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Object, 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\Object $prototype The prototype for this object
16
 * @property array $properties
17
 */
18
final class Object
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 Object 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 14
    public function __construct($properties = [])
44
    {
45 14
        foreach ($properties as $property => $value) {
46 12
            if ( !is_numeric( $property ) && $property!='properties' ) {
47 12
                 if ( $property[0] == ':' ) {
48 2
                    $property = substr($property, 1);
49 2
                    $this->_staticMethods[$property] = true;
50 2
                    $this->_ownProperties[$property] = $value;
51 12
                } else if ($property == 'prototype') {
52 4
                    $this->prototype = $value;
53
                } else {
54 12
                    $this->_ownProperties[$property] = $this->_bind( $value );
55
                }
56
            }
57
        }
58 14
    }
59
60
    /**
61
     * @param $name
62
     * @param $args
63
     * @return mixed
64
     * @throws \arc\ExceptionMethodNotFound
65
     */
66 5
    public function __call($name, $args)
67
    {
68 5
        if (array_key_exists( $name, $this->_ownProperties ) && is_callable( $this->_ownProperties[$name] )) {
69 5
            if ( array_key_exists($name, $this->_staticMethods) ) {
70 1
                array_unshift($args, $this);
71
            }
72 5
            return call_user_func_array( $this->_ownProperties[$name], $args );
73 2
        } elseif (is_object( $this->prototype)) {
74 2
            $method = $this->_getPrototypeProperty( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $method is correct as $this->_getPrototypeProperty($name) (which targets arc\prototype\Object::_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 2
            if (is_callable( $method )) {
76 2
                if ( array_key_exists($name, $this->_staticMethods) ) {
77
                    array_unshift($args, $this);
78
                }
79 2
                return call_user_func_array( $method, $args );
80
            }
81
        }
82
        throw new \arc\ExceptionMethodNotFound( $name.' is not a method on this Object', \arc\exceptions::OBJECT_NOT_FOUND );
83
    }
84
85
    /**
86
     * @param $name
87
     * @return array|null|Object
88
     */
89 11
    public function __get($name)
90
    {
91
        switch ($name) {
92 11
            case 'prototype':
93
                return $this->prototype;
94
            break;
1 ignored issue
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 10
            case 'properties':
96 2
                return $this->_getPublicProperties();
97
            break;
1 ignored issue
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...
98
            default:
99 10
                if ( array_key_exists($name, $this->_ownProperties) ) {
100 10
                    $property = $this->_ownProperties[$name];
101 10
                    if ( is_array($property) ) {
102 3
                        if ( isset($property['get']) && is_callable($property['get']) ) {
103 2
                            $getter = \Closure::bind( $property['get'], $this, $this );
104 2
                            return $getter();
105 1
                        } else if ( isset($property[':get']) && is_callable($property[':get']) ) {
106 1
                            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 7
                    return $property;    
113
                }
114 1
                return $this->_getPrototypeProperty( $name );
115
            break;
1 ignored issue
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...
116
        }
117
    }
118
119 4
    private function _isGetterOrSetter($property) {
120
        return (
121 4
            isset($property)
122 4
            && is_array($property) 
123
            && ( 
124 3
                 ( isset($property['get']) && is_callable($property['get']) )
125 1
                || ( isset($property[':get']) && is_callable($property[':get']) )
126
                || ( isset($property['set']) && is_callable($property['set']) )
127 4
                || ( isset($property[':set']) && is_callable($property[':set']) )
128
            )
129
        );
130
    }
131
132
    /**
133
     * @param $name
134
     * @param $value
135
     */
136 5
    public function __set($name, $value)
137
    {
138 5
        if (in_array( $name, [ 'prototype', 'properties' ] )) {
139
            return;
140
        }
141 5
        if ( !isset($this->_ownProperties[$name]) && !\arc\prototype::isExtensible($this) ) {
142 1
            return;
143
        }
144 4
        $valueIsSetterOrGetter = $this->_isGetterOrSetter($value);
145 4
        $propertyIsSetterOrGetter = (isset($this->_ownProperties[$name]) 
146 4
            ? $this->_isGetterOrSetter($this->_ownProperties[$name])
147 4
            : false
148
        );
149 4
        if ( \arc\prototype::isSealed($this) && $valueIsSetterOrGetter!=$propertyIsSetterOrGetter ) {
150
            return;
151
        }
152 4
        $changes = [];
153 4
        $changes['name'] = $name;
154 4
        $changes['object'] = $this;
155 4
        if ( array_key_exists($name, $this->_ownProperties) ) {
156 4
            $changes['type'] = 'update';
157 4
            $changes['oldValue'] = $this->_ownProperties[$name];
158
        } else {
159 1
            $changes['type'] = 'add';
160
        }
161
162 4
        $clearcache = false;
163
        // get current value for $name, to check if it has a getter and/or a setter
164 4 View Code Duplication
        if ( array_key_exists($name, $this->_ownProperties) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
165 4
            $current = $this->_ownProperties[$name];
166
        } else {
167 1
            $current = $this->_getPrototypeProperty($name);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $current is correct as $this->_getPrototypeProperty($name) (which targets arc\prototype\Object::_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...
168
        }
169 4
        if ( $valueIsSetterOrGetter ) {
170
            // reconfigure current property
171
            $clearcache = true;
172
            $this->_ownProperties[$name] = $value;
173
            unset($this->_staticMethods[$name]);
174 4
        } else if (isset($current) && isset($current['set']) && is_callable($current['set'])) {
175
            // bindable setter found, use it, no need to set anything in _ownProperties
176 1
            $setter = \Closure::bind($current['set'], $this, $this);
177 1
            $setter($value);
178 3
        } else if (isset($current) && isset($current[':set']) && is_callable($current[':set'])) {
179
            // nonbindable setter found
180 1
            $current[':set']($this, $value);
181 2
        } else if (isset($current) && ( 
182 2
            (isset($current['get']) && is_callable($current['get']) )
183 2
            || (isset($current[':get']) && is_callable($current[':get']) ) )
184
        ) {
185
            // there is only a getter, no setter, so ignore setting this property, its readonly.
186 1
            return null;
187 1
        } else if (!array_key_exists($name, $this->_staticMethods)) {
188
            // bindable value, update _ownProperties, so clearcache as well
189 1
            $clearcache = true;
190 1
            $this->_ownProperties[$name] = $this->_bind( $value );
191
        } else {
192
            // non bindable value, update _ownProperties, so clearcache as well
193
            $clearcache = true;
194
            $this->_ownProperties[$name] = $value;
195
        }
196 3
        if ( $clearcache ) {
197
            // purge prototype cache for this property - this will clear too much but cache will be filled again
198
            // clearing exactly the right entries from the cache will generally cost more performance than this
199 1
            unset( self::$properties[ $name ] );
200 1
            $observers = \arc\prototype::getObservers($this);
201 1
            if ( isset($observers[$changes['type']]) ) {
202 1
                foreach($observers[$changes['type']] as $observer) {
203 1
                    $observer($changes);
204
                }
205
            }
206
        }
207 3
    }
208
209
    /**
210
     * Returns a list of publically accessible properties of this object and its prototypes.
211
     * @return array
212
     */
213 2
    private function _getPublicProperties()
214
    {
215
        // get public properties only, so use closure to escape local scope.
216
        // the anonymous function / closure is needed to make sure that get_object_vars
217
        // only returns public properties.
218 2
        return ( is_object( $this->prototype )
219 1
            ? array_merge( $this->prototype->properties, $this->_getLocalProperties() )
220 2
            : $this->_getLocalProperties() );
221
    }
222
223
    /**
224
     * Returns a list of publically accessible properties of this object only, disregarding its prototypes.
225
     * @return array
226
     */
227 2
    private function _getLocalProperties()
228
    {
229 2
        return [ 'prototype' => $this->prototype ] + $this->_ownProperties;
230
    }
231
232
    /**
233
     * Get a property from the prototype chain and caches it.
234
     * @param $name
235
     * @return null
236
     */
237 9
    private function _getPrototypeProperty($name)
238
    {
239 9
        if (is_object( $this->prototype )) {
240
            // cache prototype access per property - allows fast but partial cache purging
241 3
            if (!array_key_exists( $name, self::$properties )) {
242 3
                self::$properties[ $name ] = new \SplObjectStorage();
243
            }
244 3
            if (!self::$properties[$name]->contains( $this->prototype )) {
245 3
                $property = $this->prototype->{$name};
246 3
                if ( $property instanceof \Closure ) {
247 2
                    if ( !array_key_exists($name, $this->prototype->_staticMethods)) {
248 2
                        $property = $this->_bind( $property );
249
                    } else {
250
                        $this->_staticMethods[$name] = true;
251
                    }
252
                }
253 3
                self::$properties[$name][ $this->prototype ] = $property;
254
            }
255 3
            return self::$properties[$name][ $this->prototype ];
256
        } else {
257 7
            return null;
258
        }
259
    }
260
261
262
    /**
263
     * @param $name
264
     * @return bool
265
     */
266 7
    public function __isset($name)
267
    {
268 7 View Code Duplication
        if ( array_key_exists($name, $this->_ownProperties) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
269 2
            return isset($this->_ownProperties[$name]);
270
        } else {
271 6
            $val = $this->_getPrototypeProperty( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $val is correct as $this->_getPrototypeProperty($name) (which targets arc\prototype\Object::_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...
272 6
            return isset( $val );
273
        }
274
    }
275
276
    /**
277
     * @param $name
278
     */
279 1
    public function __unset($name) {
280 1
        if (!in_array( $name, [ 'prototype', 'properties' ] )) {
281 1
            if ( !\arc\prototype::isSealed($this) ) {
282 1
                $oldValue = $this->_ownProperties[$name];
283 1
                if (array_key_exists($name, $this->_staticMethods)) {
284
                    unset($this->_staticMethods[$name]);
285
                }
286 1
                unset($this->_ownProperties[$name]);
287
                // purge prototype cache for this property - this will clear too much but cache will be filled again
288
                // clearing exactly the right entries from the cache will generally cost more performance than this
289 1
                unset( self::$properties[ $name ] );
290 1
                $observers = \arc\prototype::getObservers($this);
291
                $changes = [
292 1
                    'type' => 'delete',
293 1
                    'name' => $name,
294 1
                    'object' => $this,
295 1
                    'oldValue' => $oldValue
296
                ];
297 1
                foreach ($observers['delete'] as $observer) {
298 1
                    $result = $observer($changes);
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
299
                }
300
            }
301
        }
302 1
    }
303
304
    /**
305
     *
306
     */
307 6
    public function __destruct()
308
    {
309 6
    	\arc\prototype::_destroy($this);
310 6
        return $this->_tryToCall( '__destruct' );
311
    }
312
313
    /**
314
     * @return mixed
315
     */
316 2
    public function __toString()
317
    {
318 2
        return (string) $this->_tryToCall( '__toString' );
319
    }
320
321
    /**
322
     * @return mixed
323
     * @throws \arc\ExceptionMethodNotFound
324
     */
325
    public function __invoke()
326
    {
327
        if (is_callable( $this->__invoke )) {
0 ignored issues
show
Documentation introduced by
The property __invoke does not exist on object<arc\prototype\Object>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
328
            return call_user_func_array( $this->__invoke, func_get_args() );
0 ignored issues
show
Documentation introduced by
The property __invoke does not exist on object<arc\prototype\Object>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
329
        } else {
330
            throw new \arc\ExceptionMethodNotFound( 'No __invoke method found in this Object', \arc\exceptions::OBJECT_NOT_FOUND );
331
        }
332
    }
333
334
    /**
335
     *
336
     */
337
    public function __clone()
338
    {
339
        // make sure all methods are bound to $this - the new clone.
340
        foreach ($this->_ownProperties as $name => $property) {
341
            if ( $property instanceof \Closure && !$this->_staticMethods[$name] ) {
342
                $this->{$name} = $this->_bind( $property );
343
            }
344
        }
345
        $this->_tryToCall( '__clone' );
346
    }
347
348
    /**
349
     * Binds the property to this object
350
     * @param $property
351
     * @return mixed
352
     */
353 13
    private function _bind($property)
354
    {
355 13
        if ($property instanceof \Closure ) {
356
            // make sure any internal $this references point to this object and not the prototype or undefined
357 5
            return \Closure::bind( $property, $this );
358
        }
359
360 10
        return $property;
361
    }
362
363
    /**
364
     * Only call $f if it is a callable.
365
     * @param $f
366
     * @param array $args
367
     * @return mixed
368
     */
369 7
    private function _tryToCall($name, $args = [])
370
    {
371 7
        if ( isset($this->{$name}) && is_callable( $this->{$name} )) {
372 2
            if ( array_key_exists($name, $this->_staticMethods) ) {
373 1
                array_unshift($args, $this);
374
            }
375 2
            return call_user_func_array( $this->{$name}, $args );
376
        }
377 6
    }
378
}
379
380
/**
381
 * Class dummy
382
 * This class is needed because in PHP7 you can no longer bind to \stdClass
383
 * And anonymous classes are syntax errors in PHP5.6, so there.
384
 * @package arc\lambda
385
 */
386
class dummy {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
387
}