1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie;
13
14 trait PrototypeTrait
15 {
16 private $prototype;
17
18 /**
19 * Returns the prototype associated with the class.
20 *
21 * @return Prototype
22 */
23 protected function get_prototype()
24 {
25 if (!$this->prototype)
26 {
27 $this->prototype = Prototype::from($this);
28 }
29
30 return $this->prototype;
31 }
32
33 /**
34 * The method returns an array of key/key pairs.
35 *
36 * Properties for which a lazy getter is defined are discarted. For instance, if the property
37 * `next` is defined and the class of the instance defines the getter `lazy_get_next()`, the
38 * property is discarted.
39 *
40 * Note that façade properties are also included.
41 *
42 * Warning: The code used to export private properties seams to produce frameless exception on
43 * session close. If you encounter this problem you might want to override the method. Don't
44 * forget to remove the prototype property!
45 *
46 * @return array
47 */
48 public function __sleep()
49 {
50 $keys = array_keys(get_object_vars($this));
51
52 if ($keys)
53 {
54 $keys = array_combine($keys, $keys);
55
56 unset($keys['prototype']);
57
58 foreach ($keys as $key)
59 {
60 #
61 # we don't use {@link has_method()} because using prototype during session write
62 # seams to corrupt PHP (tested with PHP 5.3.3).
63 #
64
65 if (method_exists($this, 'lazy_get_' . $key))
66 {
67 unset($keys[$key]);
68 }
69 }
70 }
71
72 foreach (Object::resolve_facade_properties($this) as $name => $property)
73 {
74 $keys[$name] = "\x00" . $property->class . "\x00" . $name;
75 }
76
77 return $keys;
78 }
79
80 /**
81 * Unsets null properties for which a getter is defined so that it is called when the property
82 * is accessed.
83 */
84 public function __wakeup()
85 {
86 $vars = get_object_vars($this);
87
88 foreach ($vars as $key => $value)
89 {
90 if ($value !== null)
91 {
92 continue;
93 }
94
95 if ($this->has_method('lazy_get_' . $key))
96 {
97 unset($this->$key);
98 }
99 }
100 }
101
102 /**
103 * If a property exists with the name specified by `$method` and holds an object which class
104 * implements `__invoke` then the object is called with the arguments. Otherwise, calls are
105 * forwarded to the {@link $prototype}.
106 *
107 * @param string $method
108 * @param array $arguments
109 *
110 * @return mixed
111 */
112 public function __call($method, $arguments)
113 {
114 if (isset($this->$method) && is_callable([ $this->$method, '__invoke' ]))
115 {
116 return call_user_func_array($this->$method, $arguments);
117 }
118
119 array_unshift($arguments, $this);
120
121 $prototype = $this->prototype ?: $this->get_prototype();
122
123 return call_user_func_array($prototype[$method], $arguments);
124 }
125
126 /**
127 * Returns the value of an inaccessible property.
128 *
129 * Multiple callbacks are tried in order to retrieve the value of the property:
130 *
131 * 1. `get_<property>`: Get and return the value of the property.
132 * 2. `lazy_get_<property>`: Get, set and return the value of the property. Because new
133 * properties are created as public the callback is only called once which is ideal for lazy
134 * loading.
135 * 3. The prototype is queried for callbacks for the `get_<property>` and
136 * `lazy_get_<property>` methods.
137 * 4. Finally, the `ICanBoogie\Object::property` event is fired to try and retrieve the value
138 * of the property.
139 *
140 * @param string $property
141 *
142 * @throws PropertyNotReadable when the property has a protected or private scope and
143 * no suitable callback could be found to retrieve its value.
144 *
145 * @throws PropertyNotDefined when the property is undefined and there is no suitable
146 * callback to retrieve its values.
147 *
148 * @return mixed The value of the inaccessible property.
149 */
150 public function __get($property)
151 {
152 $method = 'get_' . $property;
153
154 if (method_exists($this, $method))
155 {
156 return $this->$method();
157 }
158
159 $method = 'lazy_get_' . $property;
160
161 if (method_exists($this, $method))
162 {
163 return $this->$property = $this->$method();
164 }
165
166 #
167 # we didn't find a suitable method in the class, maybe the prototype has one.
168 #
169
170 $prototype = $this->prototype ?: $this->get_prototype();
171
172 $method = 'get_' . $property;
173
174 if (isset($prototype[$method]))
175 {
176 return call_user_func($prototype[$method], $this, $property);
177 }
178
179 $method = 'lazy_get_' . $property;
180
181 if (isset($prototype[$method]))
182 {
183 return $this->$property = call_user_func($prototype[$method], $this, $property);
184 }
185
186 $success = false;
187 $value = $this->last_chance_get($property, $success);
188
189 if ($success)
190 {
191 return $value;
192 }
193
194 #
195 # We tried, but the property really is unaccessible.
196 #
197
198 $reflexion_class = new \ReflectionClass($this);
199
200 try
201 {
202 $reflexion_property = $reflexion_class->getProperty($property);
203
204 if (!$reflexion_property->isPublic())
205 {
206 throw new PropertyNotReadable([ $property, $this ]);
207 }
208 }
209 catch (\ReflectionException $e) { }
210
211 if ($this->has_method('set_' . $property))
212 {
213 throw new PropertyNotReadable([ $property, $this ]);
214 }
215
216 $properties = array_keys(get_object_vars($this));
217
218 if ($properties)
219 {
220 throw new PropertyNotDefined(sprintf('Unknown or inaccessible property "%s" for object of class "%s" (available properties: %s).', $property, get_class($this), implode(', ', $properties)));
221 }
222
223 throw new PropertyNotDefined([ $property, $this ]);
224 }
225
226 /**
227 * Sets the value of an inaccessible property.
228 *
229 * The method is called because the property does not exists, it's visibility is
230 * "protected" or "private", or because although its visibility is "public" is was unset
231 * and is now inaccessible.
232 *
233 * The method only sets the property if it isn't defined by the class or its visibility is
234 * "public", but one can provide setters to override this behaviour:
235 *
236 * The `set_<property>` setter can be used to set properties that are protected or private,
237 * which can be used to make properties write-only for example.
238 *
239 * The `volatile_set_<property>` setter can be used the handle virtual properties e.g. a
240 * `minute` property that would alter a `second` property for example.
241 *
242 * The setters can be defined by the class or its prototype.
243 *
244 * Note: Permission is granted if a `lazy_get_<property>` getter is defined by the class or
245 * its prototype.
246 *
247 * @param string $property
248 * @param mixed $value
249 *
250 * @throws PropertyNotWritable if the property is not writable.
251 */
252 public function __set($property, $value)
253 {
254 $method = 'set_' . $property;
255
256 if ($this->has_method($method))
257 {
258 return $this->$method($value);
259 }
260
261 $method = 'lazy_set_' . $property;
262
263 if ($this->has_method($method))
264 {
265 return $this->$property = $this->$method($value);
266 }
267
268 $success = false;
269 $this->last_chance_set($property, $value, $success);
270
271 if ($success)
272 {
273 return;
274 }
275
276 #
277 # We tried, but the property really is unaccessible.
278 #
279
280 if (property_exists($this, $property) && !$this->has_method('lazy_get_' . $property))
281 {
282 $reflection = new \ReflectionObject($this);
283 $property_reflection = $reflection->getProperty($property);
284
285 if (!$property_reflection->isPublic())
286 {
287 throw new PropertyNotWritable([ $property, $this ]);
288 }
289
290 $this->$property = $value;
291
292 return;
293 }
294
295 if ($this->has_method('get_' . $property))
296 {
297 throw new PropertyNotWritable([ $property, $this ]);
298 }
299
300 $this->$property = $value;
301 }
302
303 /**
304 * Checks if the object has the specified property.
305 *
306 * The difference with the `property_exists()` function is that this method also checks for
307 * getters defined by the class or the prototype.
308 *
309 * @param string $property The property to check.
310 *
311 * @return bool true if the object has the property, false otherwise.
312 */
313 public function has_property($property)
314 {
315 if (property_exists($this, $property))
316 {
317 return true;
318 }
319
320 if ($this->has_method('get_' . $property) || $this->has_method('lazy_get_' . $property))
321 {
322 return true;
323 }
324
325 $success = false;
326 $this->last_chance_get($property, $success);
327
328 return $success;
329 }
330
331 /**
332 * Checks whether this object supports the specified method.
333 *
334 * The method checks for method defined by the class and the prototype.
335 *
336 * @param string $method Name of the method.
337 *
338 * @return bool
339 */
340 public function has_method($method)
341 {
342 if (method_exists($this, $method))
343 {
344 return true;
345 }
346
347 $prototype = $this->prototype ?: $this->get_prototype();
348
349 return isset($prototype[$method]);
350 }
351
352 /**
353 * The method is invoked as a last chance to get a property,
354 * just before an exception is thrown.
355 *
356 * The method uses the helper {@link Prototype\last_chance_get()}.
357 *
358 * @param string $property Property to get.
359 * @param bool $success If the _last chance get_ was successful.
360 *
361 * @return mixed
362 */
363 protected function last_chance_get($property, &$success)
364 {
365 return Prototype\last_chance_get($this, $property, $success);
366 }
367
368 /**
369 * The method is invoked as a last chance to set a property,
370 * just before an exception is thrown.
371 *
372 * The method uses the helper {@link Prototype\last_chance_set()}.
373 *
374 * @param string $property Property to set.
375 * @param mixed $value Value of the property.
376 * @param bool $success If the _last chance set_ was successful.
377 */
378 protected function last_chance_set($property, $value, &$success)
379 {
380 Prototype\last_chance_set($this, $property, $value, $success);
381 }
382 }