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
|
|||
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 |
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.