Completed
Push — master ( 53c47c...480bd5 )
by Filip
9s
created

MagicAccessors::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 3
rs 10
c 1
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * This file is part of the Kdyby (http://www.kdyby.org)
5
 * Copyright (c) 2008 Filip Procházka ([email protected])
6
 * For the full copyright and license information, please view the file license.txt that was distributed with this source code.
7
 */
8
9
namespace Kdyby\Doctrine\Entities;
10
11
use Doctrine;
12
use Doctrine\Common\Collections\Collection;
13
use Doctrine\ORM\Mapping as ORM;
14
use Kdyby;
15
use Kdyby\Doctrine\Collections\ReadOnlyCollectionWrapper;
16
use Kdyby\Doctrine\MemberAccessException;
17
use Kdyby\Doctrine\UnexpectedValueException;
18
use Nette;
19
use Nette\Utils\Callback;
20
use Nette\Utils\ObjectMixin;
21
22
23
24
/**
25
 * @author Filip Procházka <[email protected]>
26
 */
27
trait MagicAccessors
28
{
29
30
	/**
31
	 * @var array
32
	 */
33
	private static $__properties = [];
34
35
	/**
36
	 * @var array
37
	 */
38
	private static $__methods = [];
39
40
41
42
	/**
43
	 * @param string $property property name
44
	 * @param array $args
45
	 * @return Collection|array
46
	 */
47
	protected function convertCollection($property, array $args = NULL)
0 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
48
	{
49
		return new ReadOnlyCollectionWrapper($this->$property);
50
	}
51
52
53
54
	/**
55
	 * Utility method, that can be replaced with `::class` since php 5.5
56
	 * @return string
57
	 */
58
	public static function getClassName()
59
	{
60
		return get_called_class();
61
	}
62
63
64
65
	/**
66
	 * Access to reflection.
67
	 *
68
	 * @return Nette\Reflection\ClassType|\ReflectionClass
69
	 */
70
	public static function getReflection()
71
	{
72
		$class = class_exists('Nette\Reflection\ClassType') ? 'Nette\Reflection\ClassType' : 'ReflectionClass';
73
		return new $class(get_called_class());
74
	}
75
76
77
78
	/**
79
	 * Allows the user to access through magic methods to protected and public properties.
80
	 * There are get<name>() and set<name>($value) methods for every protected or public property,
81
	 * and for protected or public collections there are add<name>($entity), remove<name>($entity) and has<name>($entity).
82
	 * When you'll try to call setter on collection, or collection manipulator on generic value, it will throw.
83
	 * Getters on collections will return all it's items.
84
	 *
85
	 * @param string $name method name
86
	 * @param array $args arguments
87
	 *
88
	 * @throws \Kdyby\Doctrine\UnexpectedValueException
89
	 * @throws \Kdyby\Doctrine\MemberAccessException
90
	 * @return mixed
91
	 */
92
	public function __call($name, $args)
93
	{
94
		if (strlen($name) > 3) {
95
			$properties = $this->listObjectProperties();
96
97
			$op = substr($name, 0, 3);
98
			$prop = strtolower($name[3]) . substr($name, 4);
99
			if ($op === 'set' && isset($properties[$prop])) {
100
				if ($this->$prop instanceof Collection) {
101
					throw UnexpectedValueException::collectionCannotBeReplaced($this, $prop);
102
				}
103
104
				$this->$prop = $args[0];
105
106
				return $this;
107
108
			} elseif ($op === 'get' && isset($properties[$prop])) {
109
				if ($this->$prop instanceof Collection) {
110
					return $this->convertCollection($prop, $args);
111
112
				} else {
113
					return $this->$prop;
114
				}
115
116
			} else { // collections
117
				if ($op === 'add') {
118
					if (isset($properties[$prop . 's'])) {
119
						if (!$this->{$prop . 's'} instanceof Collection) {
120
							throw UnexpectedValueException::notACollection($this, $prop . 's');
121
						}
122
123
						$this->{$prop . 's'}->add($args[0]);
124
125
						return $this;
126
127
					} elseif (substr($prop, -1) === 'y' && isset($properties[$prop = substr($prop, 0, -1) . 'ies'])) {
128
						if (!$this->$prop instanceof Collection) {
129
							throw UnexpectedValueException::notACollection($this, $prop);
130
						}
131
132
						$this->$prop->add($args[0]);
133
134
						return $this;
135
136
					} elseif (isset($properties[$prop])) {
137
						throw UnexpectedValueException::notACollection($this, $prop);
138
					}
139
140
				} elseif ($op === 'has') {
141
					if (isset($properties[$prop . 's'])) {
142
						if (!$this->{$prop . 's'} instanceof Collection) {
143
							throw UnexpectedValueException::notACollection($this, $prop . 's');
144
						}
145
146
						return $this->{$prop . 's'}->contains($args[0]);
147
148
					} elseif (substr($prop, -1) === 'y' && isset($properties[$prop = substr($prop, 0, -1) . 'ies'])) {
149
						if (!$this->$prop instanceof Collection) {
150
							throw UnexpectedValueException::notACollection($this, $prop);
151
						}
152
153
						return $this->$prop->contains($args[0]);
154
155
					} elseif (isset($properties[$prop])) {
156
						throw UnexpectedValueException::notACollection($this, $prop);
157
					}
158
159
				} elseif (strlen($name) > 6 && ($op = substr($name, 0, 6)) === 'remove') {
160
					$prop = strtolower($name[6]) . substr($name, 7);
161
162
					if (isset($properties[$prop . 's'])) {
163
						if (!$this->{$prop . 's'} instanceof Collection) {
164
							throw UnexpectedValueException::notACollection($this, $prop . 's');
165
						}
166
167
						$this->{$prop . 's'}->removeElement($args[0]);
168
169
						return $this;
170
171
					} elseif (substr($prop, -1) === 'y' && isset($properties[$prop = substr($prop, 0, -1) . 'ies'])) {
172
						if (!$this->$prop instanceof Collection) {
173
							throw UnexpectedValueException::notACollection($this, $prop);
174
						}
175
176
						$this->$prop->removeElement($args[0]);
177
178
						return $this;
179
180
					} elseif (isset($properties[$prop])) {
181
						throw UnexpectedValueException::notACollection($this, $prop);
182
					}
183
				}
184
			}
185
		}
186
187
		if ($name === '') {
188
			throw MemberAccessException::callWithoutName($this);
189
		}
190
		$class = get_class($this);
191
192
		// event functionality
193
		if (preg_match('#^on[A-Z]#', $name) && property_exists($class, $name)) {
194
			$rp = new \ReflectionProperty($this, $name);
195
			if ($rp->isPublic() && !$rp->isStatic()) {
196
				if (is_array($list = $this->$name) || $list instanceof \Traversable) {
197
					foreach ($list as $handler) {
198
						Callback::invokeArgs($handler, $args);
199
					}
200
				} elseif ($list !== NULL) {
201
					throw UnexpectedValueException::invalidEventValue($list, $this, $name);
202
				}
203
204
				return NULL;
205
			}
206
		}
207
208
		// extension methods
209
		if ($cb = static::extensionMethod($name)) {
210
			/** @var \Nette\Callback $cb */
211
			array_unshift($args, $this);
212
213
			return call_user_func_array($cb, $args);
214
		}
215
216
		throw MemberAccessException::undefinedMethodCall($this, $name);
217
	}
218
219
220
221
	/**
222
	 * Call to undefined static method.
223
	 *
224
	 * @param  string  method name (in lower case!)
225
	 * @param  array   arguments
226
	 * @return mixed
227
	 * @throws MemberAccessException
228
	 */
229
	public static function __callStatic($name, $args)
230
	{
231
		return ObjectMixin::callStatic(get_called_class(), $name, $args);
232
	}
233
234
235
236
	/**
237
	 * Adding method to class.
238
	 *
239
	 * @param  string  method name
240
	 * @param  callable
241
	 * @return mixed
242
	 */
243
	public static function extensionMethod($name, $callback = NULL)
244
	{
245
		if (strpos($name, '::') === FALSE) {
246
			$class = get_called_class();
247
		} else {
248
			list($class, $name) = explode('::', $name);
249
			$class = (new \ReflectionClass($class))->getName();
250
		}
251
		if ($callback === NULL) {
252
			return ObjectMixin::getExtensionMethod($class, $name);
253
		} else {
254
			ObjectMixin::setExtensionMethod($class, $name, $callback);
255
		}
256
	}
257
258
259
260
	/**
261
	 * Returns property value. Do not call directly.
262
	 *
263
	 * @param string $name property name
264
	 *
265
	 * @throws MemberAccessException if the property is not defined.
266
	 * @return mixed property value
267
	 */
268
	public function &__get($name)
269
	{
270
		if ($name === '') {
271
			throw MemberAccessException::propertyReadWithoutName($this);
272
		}
273
274
		// property getter support
275
		$originalName = $name;
276
		$name[0] = $name[0] & "\xDF"; // case-sensitive checking, capitalize first character
277
		$m = 'get' . $name;
278
279
		$methods = $this->listObjectMethods();
280
		if (isset($methods[$m])) {
281
			// ampersands:
282
			// - uses &__get() because declaration should be forward compatible (e.g. with Nette\Utils\Html)
283
			// - doesn't call &$_this->$m because user could bypass property setter by: $x = & $obj->property; $x = 'new value';
284
			$val = $this->$m();
285
286
			return $val;
287
		}
288
289
		$m = 'is' . $name;
290
		if (isset($methods[$m])) {
291
			$val = $this->$m();
292
293
			return $val;
294
		}
295
296
		// protected attribute support
297
		$properties = $this->listObjectProperties();
298
		if (isset($properties[$name = $originalName])) {
299
			if ($this->$name instanceof Collection) {
300
				$coll = $this->convertCollection($name);
301
302
				return $coll;
303
304
			} else {
305
				$val = $this->$name;
306
307
				return $val;
308
			}
309
		}
310
311
		$type = isset($methods['set' . $name]) ? 'a write-only' : 'an undeclared';
312
		throw MemberAccessException::propertyNotReadable($type, $this, $originalName);
313
	}
314
315
316
317
	/**
318
	 * Sets value of a property. Do not call directly.
319
	 *
320
	 * @param string $name property name
321
	 * @param mixed $value property value
322
	 *
323
	 * @throws UnexpectedValueException
324
	 * @throws MemberAccessException if the property is not defined or is read-only
325
	 */
326
	public function __set($name, $value)
327
	{
328
		if ($name === '') {
329
			throw MemberAccessException::propertyWriteWithoutName($this);
330
		}
331
332
		// property setter support
333
		$originalName = $name;
334
		$name[0] = $name[0] & "\xDF"; // case-sensitive checking, capitalize first character
335
336
		$methods = $this->listObjectMethods();
337
		$m = 'set' . $name;
338
		if (isset($methods[$m])) {
339
			$this->$m($value);
340
341
			return;
342
		}
343
344
		// protected attribute support
345
		$properties = $this->listObjectProperties();
346
		if (isset($properties[$name = $originalName])) {
347
			if ($this->$name instanceof Collection) {
348
				throw UnexpectedValueException::collectionCannotBeReplaced($this, $name);
349
			}
350
351
			$this->$name = $value;
352
353
			return;
354
		}
355
356
		$type = isset($methods['get' . $name]) || isset($methods['is' . $name]) ? 'a read-only' : 'an undeclared';
357
		throw MemberAccessException::propertyNotWritable($type, $this, $originalName);
358
	}
359
360
361
362
	/**
363
	 * Is property defined?
364
	 *
365
	 * @param string $name property name
366
	 *
367
	 * @return bool
368
	 */
369
	public function __isset($name)
370
	{
371
		$properties = $this->listObjectProperties();
372
		if (isset($properties[$name])) {
373
			return TRUE;
374
		}
375
376
		if ($name === '') {
377
			return FALSE;
378
		}
379
380
		$methods = $this->listObjectMethods();
381
		$name[0] = $name[0] & "\xDF";
382
383
		return isset($methods['get' . $name]) || isset($methods['is' . $name]);
384
	}
385
386
387
388
	/**
389
	 * Access to undeclared property.
390
	 *
391
	 * @param  string  property name
392
	 * @return void
393
	 * @throws MemberAccessException
394
	 */
395
	public function __unset($name)
396
	{
397
		ObjectMixin::remove($this, $name);
398
	}
399
400
401
402
	/**
403
	 * Should return only public or protected properties of class
404
	 *
405
	 * @return array
406
	 */
407
	private function listObjectProperties()
408
	{
409
		$class = get_class($this);
410
		if (!isset(self::$__properties[$class])) {
411
			$refl = new \ReflectionClass($class);
412
			$properties = array_map(function (\ReflectionProperty $property) {
413
				return $property->getName();
414
			}, $refl->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED));
415
416
			self::$__properties[$class] = array_flip($properties);
417
		}
418
419
		return self::$__properties[$class];
420
	}
421
422
423
424
	/**
425
	 * Should return all public methods of class
426
	 *
427
	 * @return array
428
	 */
429
	private function listObjectMethods()
430
	{
431
		$class = get_class($this);
432
		if (!isset(self::$__methods[$class])) {
433
			$refl = new \ReflectionClass($class);
434
			$methods = array_map(function (\ReflectionMethod $method) {
435
				return $method->getName();
436
			}, $refl->getMethods(\ReflectionMethod::IS_PUBLIC));
437
438
			self::$__methods[$class] = array_flip($methods);
439
		}
440
441
		return self::$__methods[$class];
442
	}
443
444
}
445