Completed
Push — master ( 1ba1ea...0e3150 )
by Auke
03:41
created

src/lambda/Prototype.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 1
    public function hasOwnProperty($name)
45
    {
46 1
        $props = $this->getLocalProperties();
47
48 1
        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 3
    public function extend($properties = [])
57
    {
58 3
        $properties['prototype'] = $this;
59 3
        $descendant = new static($properties);
60
61 3
        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 5
    public function __construct($properties = [])
85
    {
86 5
        foreach ($properties as $property => $value) {
87 5
            if ( !is_numeric( $property ) && $property!='properties' ) {
88 5
                 if ( $property[0] == ':' ) {
89
                    $property = substr($property, 1);
90
                    $this->{$property} = $value;
91
                } else {
92 5
                    $this->{$property} = $this->_bind( $value );
93
                }
94 5
            }
95 5
        }
96 5
    }
97
98
    /**
99
     * @param $name
100
     * @param $args
101
     * @return mixed
102
     * @throws \arc\ExceptionMethodNotFound
103
     */
104 4
    public function __call($name, $args)
105
    {
106 4
        if (isset( $this->{$name} ) && is_callable( $this->{$name} )) {
107 4
            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 3
    public function __get($name)
122
    {
123
        switch ($name) {
124 3
            case 'prototype':
125
                return $this->prototype;
126
            break;
127 3
            case 'properties':
128
                return $this->getPublicProperties();
129
            break;
130 3
            default:
131 3
                return $this->getPrototypeProperty( $name );
132
            break;
133 3
        }
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( $this ) )
0 ignored issues
show
The call to Prototype::getLocalProperties() has too many arguments starting with $this.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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 1
        $getLocalProperties = \Closure::bind( function ($o) {
157 1
                return get_object_vars($o);
158 1
            }, new \stdClass(), new \stdClass() );
159
160 1
        return [ 'prototype' => $this->prototype ] + $getLocalProperties( $this );
161
    }
162
163
    /**
164
     * Get a property from the prototype chain and caches it.
165
     * @param $name
166
     * @return null
167
     */
168 3
    private function getPrototypeProperty($name)
169
    {
170 3
        if (is_object( $this->prototype )) {
171
            // cache prototype access per property - allows fast but partial cache purging
172 3
            if (!array_key_exists( $name, self::$properties )) {
173 3
                self::$properties[ $name ] = new \SplObjectStorage();
174 3
            }
175 3
            if (!self::$properties[$name]->contains( $this->prototype )) {
176 3
                self::$properties[$name][ $this->prototype ] = $this->_bind( $this->prototype->{$name} );
177 3
            }
178 3
            return self::$properties[$name][ $this->prototype ];
179
        } else {
180 1
            return null;
181
        }
182
    }
183
184
    /**
185
     * @param $name
186
     * @param $value
187
     */
188 5
    public function __set($name, $value)
189
    {
190 5
        if (!in_array( $name, [ 'prototype', 'properties' ] )) {
191 5
            $this->{$name} = $this->_bind( $value );
192
            // purge prototype cache for this property - this will clear too much but cache will be filled again
193
            // clearing exactly the right entries from the cache will generally cost more performance than this
194 5
            unset( self::$properties[ $name ] );
195 5
        }
196 5
    }
197
198
    /**
199
     * @param $name
200
     * @return bool
201
     */
202 2
    public function __isset($name)
203
    {
204 2
        $val = $this->getPrototypeProperty( $name );
205
206 2
        return isset( $val );
207
    }
208
209
    /**
210
     *
211
     */
212 1
    public function __destruct()
213
    {
214 1
        return $this->_tryToCall( $this->__destruct );
215
    }
216
217
    /**
218
     * @return mixed
219
     */
220 1
    public function __toString()
221
    {
222 1
        return $this->_tryToCall( $this->__toString );
223
    }
224
225
    /**
226
     * @return mixed
227
     * @throws \arc\ExceptionMethodNotFound
228
     */
229
    public function __invoke()
230
    {
231
        if (is_callable( $this->__invoke )) {
232
            return call_user_func_array( $this->__invoke, func_get_args() );
233
        } else {
234
            throw new \arc\ExceptionMethodNotFound( 'No __invoke method found in this Object', \arc\exceptions::OBJECT_NOT_FOUND );
235
        }
236
    }
237
238
    /**
239
     *
240
     */
241
    public function __clone()
242
    {
243
        // make sure all methods are bound to $this - the new clone.
244
        foreach (get_object_vars( $this ) as $property) {
245
            $this->{$property} = $this->_bind( $property );
246
        }
247
        $this->_tryToCall( $this->__clone );
248
    }
249
250
    /**
251
     * Binds the property to this object
252
     * @param $property
253
     * @return mixed
254
     */
255 5
    private function _bind($property)
256
    {
257 5
        if ($property instanceof \Closure) {
258
            // make sure any internal $this references point to this object and not the prototype or undefined
259 5
            return \Closure::bind( $property, $this );
260
        }
261
262 4
        return $property;
263
    }
264
265
    /**
266
     * Only call $f if it is a callable.
267
     * @param $f
268
     * @param array $args
269
     * @return mixed
270
     */
271 2
    private function _tryToCall($f, $args = [])
272
    {
273 2
        if (is_callable( $f )) {
274 1
            return call_user_func_array( $f, $args );
275
        }
276 1
    }
277
}
278