Completed
Pull Request — master (#23)
by Auke
02:10
created

dummy

Complexity

Total Complexity 0

Size/Duplication

Total Lines 2
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 0
c 1
b 0
f 1
lcom 0
cbo 0
dl 0
loc 2
ccs 0
cts 0
cp 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\lambda;
11
12
/**
13
 * This class allows you to create throw-away objects with methods and properties. It is meant to be used
14
 * as a way to create rendering objects for a certain data set. e.g.
15
 * <code>
16
 * $view = \arc\lambda::prototype( [
17
 *		'menu' => function ($children) {
18
 *			return \arc\html::ul(['class' => 'menu'], array_map( $this->menuitem, (array) $children ) );
19
 *		},
20
 *		'menuitem' => function ($input) {
21
 *			return \arc\html::li( $this->menulink( $input ), ( isset( $input['children'] ) ? $this->menu( $input['children'] ) : null ) );
22
 *		},
23
 *		'menulink' => function ($input) {
24
 *			return \arc\html::a( [ 'href' => $input['url'] ], $input['name'] );
25
 *		}
26
 * ] );
27
 * echo $view->menu( $menulist );
28
 * </code>
29
 */
30
final class Prototype
31
{
32
    private static $properties = [];
33
34
    /**
35
    * @var Object prototype Readonly reference to a prototype object. Can only be set in the constructor.
36
    */
37
    private $prototype = null;
38
39
    /**
40
     * Returns true if the named property is set in this object, disregarding the prototype chain
41
     * @param $name
42
     * @return bool
43
     */
44
    public function hasOwnProperty($name)
45
    {
46
        $props = $this->getLocalProperties();
47
48
        return isset( $props[$name] );
49
    }
50
51
    /**
52
     * Creates a new prototype object extending this one.
53
     * @param array $properties
54
     * @return static
55
     */
56
    public function extend($properties = [])
57
    {
58
        $properties['prototype'] = $this;
59
        $descendant = new static($properties);
60
61
        return $descendant;
62
    }
63
64
    /**
65
     * Returns true if the current object has the given object somewhere in its prototype chain.
66
     * @param $object
67
     * @return bool
68
     */
69
    public function hasPrototype($object)
70
    {
71
        if (!$this->prototype) {
72
            return false;
73
        }
74
        if ($this->prototype === $object) {
75
            return true;
76
        }
77
78
        return $this->prototype->hasPrototype( $object );
79
    }
80
81
    /**
82
     * @param array $properties
83
     */
84
    public function __construct($properties = [])
85
    {
86
        foreach ($properties as $property => $value) {
87
            if ( !is_numeric( $property ) && $property!='properties' ) {
88
                 if ( $property[0] == ':' ) {
89
                    $property = substr($property, 1);
90
                    $this->{$property} = $value;
91
                } else {
92
                    $this->{$property} = $this->_bind( $value );
93
                }
94
            }
95
        }
96
    }
97
98
    /**
99
     * @param $name
100
     * @param $args
101
     * @return mixed
102
     * @throws \arc\ExceptionMethodNotFound
103
     */
104
    public function __call($name, $args)
105
    {
106
        if (isset( $this->{$name} ) && is_callable( $this->{$name} )) {
107
            return call_user_func_array( $this->{$name}, $args );
108
        } elseif (is_object( $this->prototype)) {
109
            $method = $this->_bind( $this->getPrototypeProperty( $name ) );
110
            if (is_callable( $method )) {
111
                return call_user_func_array( $method, $args );
112
            }
113
        }
114
        throw new \arc\ExceptionMethodNotFound( $name.' is not a method on this Object', \arc\exceptions::OBJECT_NOT_FOUND );
115
    }
116
117
    /**
118
     * @param $name
119
     * @return array|null|Object
120
     */
121
    public function __get($name)
122
    {
123
        switch ($name) {
124
            case 'prototype':
125
                return $this->prototype;
126
            break;
127
            case 'properties':
128
                return $this->getPublicProperties();
129
            break;
130
            default:
131
                return $this->getPrototypeProperty( $name );
132
            break;
133
        }
134
    }
135
136
    /**
137
     * Returns a list of publically accessible properties of this object and its prototypes.
138
     * @return array
139
     */
140
    private function getPublicProperties()
141
    {
142
        // get public properties only, so use closure to escape local scope.
143
        // the anonymous function / closure is needed to make sure that get_object_vars
144
        // only returns public properties.
145
        return ( is_object( $this->prototype )
146
            ? array_merge( $this->prototype->properties, $this->getLocalProperties() )
147
            : $this->getLocalProperties() );
148
    }
149
150
    /**
151
     * Returns a list of publically accessible properties of this object only, disregarding its prototypes.
152
     * @return array
153
     */
154
    private function getLocalProperties()
155
    {
156
        $getLocalProperties = \Closure::bind(function ($o) {
157
            return get_object_vars($o);
158
        }, new dummy(), new dummy());
159
        return [ 'prototype' => $this->prototype ] + $getLocalProperties( $this );
160
    }
161
162
    /**
163
     * Get a property from the prototype chain and caches it.
164
     * @param $name
165
     * @return null
166
     */
167
    private function getPrototypeProperty($name)
168
    {
169
        if (is_object( $this->prototype )) {
170
            // cache prototype access per property - allows fast but partial cache purging
171
            if (!array_key_exists( $name, self::$properties )) {
172
                self::$properties[ $name ] = new \SplObjectStorage();
173
            }
174
            if (!self::$properties[$name]->contains( $this->prototype )) {
175
                self::$properties[$name][ $this->prototype ] = $this->_bind( $this->prototype->{$name} );
176
            }
177
            return self::$properties[$name][ $this->prototype ];
178
        } else {
179
            return null;
180
        }
181
    }
182
183
    /**
184
     * @param $name
185
     * @param $value
186
     */
187
    public function __set($name, $value)
188
    {
189
        if (!in_array( $name, [ 'prototype', 'properties' ] )) {
190
            $this->{$name} = $this->_bind( $value );
191
            // purge prototype cache for this property - this will clear too much but cache will be filled again
192
            // clearing exactly the right entries from the cache will generally cost more performance than this
193
            unset( self::$properties[ $name ] );
194
        }
195
    }
196
197
    /**
198
     * @param $name
199
     * @return bool
200
     */
201
    public function __isset($name)
202
    {
203
        $val = $this->getPrototypeProperty( $name );
204
205
        return isset( $val );
206
    }
207
208
    /**
209
     *
210
     */
211
    public function __destruct()
212
    {
213
        return $this->_tryToCall( $this->__destruct );
0 ignored issues
show
Documentation introduced by
The property __destruct does not exist on object<arc\lambda\Prototype>. 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...
214
    }
215
216
    /**
217
     * @return mixed
218
     */
219
    public function __toString()
220
    {
221
        return $this->_tryToCall( $this->__toString );
0 ignored issues
show
Documentation introduced by
The property __toString does not exist on object<arc\lambda\Prototype>. 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...
222
    }
223
224
    /**
225
     * @return mixed
226
     * @throws \arc\ExceptionMethodNotFound
227
     */
228
    public function __invoke()
229
    {
230
        if (is_callable( $this->__invoke )) {
0 ignored issues
show
Documentation introduced by
The property __invoke does not exist on object<arc\lambda\Prototype>. 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...
231
            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\lambda\Prototype>. 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...
232
        } else {
233
            throw new \arc\ExceptionMethodNotFound( 'No __invoke method found in this Object', \arc\exceptions::OBJECT_NOT_FOUND );
234
        }
235
    }
236
237
    /**
238
     *
239
     */
240
    public function __clone()
241
    {
242
        // make sure all methods are bound to $this - the new clone.
243
        foreach (get_object_vars( $this ) as $property) {
244
            $this->{$property} = $this->_bind( $property );
245
        }
246
        $this->_tryToCall( $this->__clone );
0 ignored issues
show
Documentation introduced by
The property __clone does not exist on object<arc\lambda\Prototype>. 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...
247
    }
248
249
    /**
250
     * Binds the property to this object
251
     * @param $property
252
     * @return mixed
253
     */
254
    private function _bind($property)
255
    {
256
        if ($property instanceof \Closure) {
257
            // make sure any internal $this references point to this object and not the prototype or undefined
258
            return \Closure::bind( $property, $this );
259
        }
260
261
        return $property;
262
    }
263
264
    /**
265
     * Only call $f if it is a callable.
266
     * @param $f
267
     * @param array $args
268
     * @return mixed
269
     */
270
    private function _tryToCall($f, $args = [])
271
    {
272
        if (is_callable( $f )) {
273
            return call_user_func_array( $f, $args );
274
        }
275
    }
276
}
277
278
/**
279
 * Class dummy
280
 * This class is needed because in PHP7 you can no longer bind to \stdClass
281
 * And anonymous classes are syntax errors in PHP5.6, so there.
282
 * @package arc\lambda
283
 */
284
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...
285
}