Completed
Push — master ( cc4a3e...ae0f00 )
by Filip
02:42
created

MagicAccessors   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 5
Bugs 2 Features 0
Metric Value
wmc 80
c 5
b 2
f 0
lcom 1
cbo 5
dl 0
loc 443
rs 4.8717

12 Methods

Rating   Name   Duplication   Size   Complexity  
A convertCollection() 0 4 1
A getClassName() 0 4 1
A getReflection() 0 5 2
D __call() 0 151 49
A __callStatic() 0 4 1
A extensionMethod() 0 14 3
C __get() 0 46 7
C __set() 0 33 7
A __isset() 0 16 4
A __unset() 0 4 1
A listObjectProperties() 0 14 2
A listObjectMethods() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like MagicAccessors often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MagicAccessors, and based on these observations, apply Extract Interface, too.

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 (substr($prop, -1) === 's' && isset($properties[$prop = substr($prop, 0, -1) . 'ses'])) {
137
						if (!$this->$prop instanceof Collection) {
138
							throw UnexpectedValueException::notACollection($this, $prop);
139
						}
140
141
						$this->$prop->add($args[0]);
142
143
						return $this;
144
145
					} elseif (isset($properties[$prop])) {
146
						throw UnexpectedValueException::notACollection($this, $prop);
147
					}
148
149
				} elseif ($op === 'has') {
150
					if (isset($properties[$prop . 's'])) {
151
						if (!$this->{$prop . 's'} instanceof Collection) {
152
							throw UnexpectedValueException::notACollection($this, $prop . 's');
153
						}
154
155
						return $this->{$prop . 's'}->contains($args[0]);
156
157
					} elseif (substr($prop, -1) === 'y' && isset($properties[$prop = substr($prop, 0, -1) . 'ies'])) {
158
						if (!$this->$prop instanceof Collection) {
159
							throw UnexpectedValueException::notACollection($this, $prop);
160
						}
161
162
						return $this->$prop->contains($args[0]);
163
164
					} elseif (substr($prop, -1) === 's' && isset($properties[$prop = substr($prop, 0, -1) . 'ses'])) {
165
						if (!$this->$prop instanceof Collection) {
166
							throw UnexpectedValueException::notACollection($this, $prop);
167
						}
168
169
						return $this->$prop->contains($args[0]);
170
171
					} elseif (isset($properties[$prop])) {
172
						throw UnexpectedValueException::notACollection($this, $prop);
173
					}
174
175
				} elseif (strlen($name) > 6 && ($op = substr($name, 0, 6)) === 'remove') {
176
					$prop = strtolower($name[6]) . substr($name, 7);
177
178
					if (isset($properties[$prop . 's'])) {
179
						if (!$this->{$prop . 's'} instanceof Collection) {
180
							throw UnexpectedValueException::notACollection($this, $prop . 's');
181
						}
182
183
						$this->{$prop . 's'}->removeElement($args[0]);
184
185
						return $this;
186
187
					} elseif (substr($prop, -1) === 'y' && isset($properties[$prop = substr($prop, 0, -1) . 'ies'])) {
188
						if (!$this->$prop instanceof Collection) {
189
							throw UnexpectedValueException::notACollection($this, $prop);
190
						}
191
192
						$this->$prop->removeElement($args[0]);
193
194
						return $this;
195
196
					} elseif (substr($prop, -1) === 's' && isset($properties[$prop = substr($prop, 0, -1) . 'ses'])) {
197
						if (!$this->$prop instanceof Collection) {
198
							throw UnexpectedValueException::notACollection($this, $prop);
199
						}
200
201
						$this->$prop->removeElement($args[0]);
202
203
						return $this;
204
205
					} elseif (isset($properties[$prop])) {
206
						throw UnexpectedValueException::notACollection($this, $prop);
207
					}
208
				}
209
			}
210
		}
211
212
		if ($name === '') {
213
			throw MemberAccessException::callWithoutName($this);
214
		}
215
		$class = get_class($this);
216
217
		// event functionality
218
		if (preg_match('#^on[A-Z]#', $name) && property_exists($class, $name)) {
219
			$rp = new \ReflectionProperty($this, $name);
220
			if ($rp->isPublic() && !$rp->isStatic()) {
221
				if (is_array($list = $this->$name) || $list instanceof \Traversable) {
222
					foreach ($list as $handler) {
223
						Callback::invokeArgs($handler, $args);
224
					}
225
				} elseif ($list !== NULL) {
226
					throw UnexpectedValueException::invalidEventValue($list, $this, $name);
227
				}
228
229
				return NULL;
230
			}
231
		}
232
233
		// extension methods
234
		if ($cb = static::extensionMethod($name)) {
235
			/** @var \Nette\Callback $cb */
236
			array_unshift($args, $this);
237
238
			return call_user_func_array($cb, $args);
239
		}
240
241
		throw MemberAccessException::undefinedMethodCall($this, $name);
242
	}
243
244
245
246
	/**
247
	 * Call to undefined static method.
248
	 *
249
	 * @param  string  method name (in lower case!)
250
	 * @param  array   arguments
251
	 * @return mixed
252
	 * @throws MemberAccessException
253
	 */
254
	public static function __callStatic($name, $args)
255
	{
256
		return ObjectMixin::callStatic(get_called_class(), $name, $args);
257
	}
258
259
260
261
	/**
262
	 * Adding method to class.
263
	 *
264
	 * @param  string  method name
265
	 * @param  callable
266
	 * @return mixed
267
	 */
268
	public static function extensionMethod($name, $callback = NULL)
269
	{
270
		if (strpos($name, '::') === FALSE) {
271
			$class = get_called_class();
272
		} else {
273
			list($class, $name) = explode('::', $name);
274
			$class = (new \ReflectionClass($class))->getName();
275
		}
276
		if ($callback === NULL) {
277
			return ObjectMixin::getExtensionMethod($class, $name);
278
		} else {
279
			ObjectMixin::setExtensionMethod($class, $name, $callback);
280
		}
281
	}
282
283
284
285
	/**
286
	 * Returns property value. Do not call directly.
287
	 *
288
	 * @param string $name property name
289
	 *
290
	 * @throws MemberAccessException if the property is not defined.
291
	 * @return mixed property value
292
	 */
293
	public function &__get($name)
294
	{
295
		if ($name === '') {
296
			throw MemberAccessException::propertyReadWithoutName($this);
297
		}
298
299
		// property getter support
300
		$originalName = $name;
301
		$name[0] = $name[0] & "\xDF"; // case-sensitive checking, capitalize first character
302
		$m = 'get' . $name;
303
304
		$methods = $this->listObjectMethods();
305
		if (isset($methods[$m])) {
306
			// ampersands:
307
			// - uses &__get() because declaration should be forward compatible (e.g. with Nette\Utils\Html)
308
			// - doesn't call &$_this->$m because user could bypass property setter by: $x = & $obj->property; $x = 'new value';
309
			$val = $this->$m();
310
311
			return $val;
312
		}
313
314
		$m = 'is' . $name;
315
		if (isset($methods[$m])) {
316
			$val = $this->$m();
317
318
			return $val;
319
		}
320
321
		// protected attribute support
322
		$properties = $this->listObjectProperties();
323
		if (isset($properties[$name = $originalName])) {
324
			if ($this->$name instanceof Collection) {
325
				$coll = $this->convertCollection($name);
326
327
				return $coll;
328
329
			} else {
330
				$val = $this->$name;
331
332
				return $val;
333
			}
334
		}
335
336
		$type = isset($methods['set' . $name]) ? 'a write-only' : 'an undeclared';
337
		throw MemberAccessException::propertyNotReadable($type, $this, $originalName);
338
	}
339
340
341
342
	/**
343
	 * Sets value of a property. Do not call directly.
344
	 *
345
	 * @param string $name property name
346
	 * @param mixed $value property value
347
	 *
348
	 * @throws UnexpectedValueException
349
	 * @throws MemberAccessException if the property is not defined or is read-only
350
	 */
351
	public function __set($name, $value)
352
	{
353
		if ($name === '') {
354
			throw MemberAccessException::propertyWriteWithoutName($this);
355
		}
356
357
		// property setter support
358
		$originalName = $name;
359
		$name[0] = $name[0] & "\xDF"; // case-sensitive checking, capitalize first character
360
361
		$methods = $this->listObjectMethods();
362
		$m = 'set' . $name;
363
		if (isset($methods[$m])) {
364
			$this->$m($value);
365
366
			return;
367
		}
368
369
		// protected attribute support
370
		$properties = $this->listObjectProperties();
371
		if (isset($properties[$name = $originalName])) {
372
			if ($this->$name instanceof Collection) {
373
				throw UnexpectedValueException::collectionCannotBeReplaced($this, $name);
374
			}
375
376
			$this->$name = $value;
377
378
			return;
379
		}
380
381
		$type = isset($methods['get' . $name]) || isset($methods['is' . $name]) ? 'a read-only' : 'an undeclared';
382
		throw MemberAccessException::propertyNotWritable($type, $this, $originalName);
383
	}
384
385
386
387
	/**
388
	 * Is property defined?
389
	 *
390
	 * @param string $name property name
391
	 *
392
	 * @return bool
393
	 */
394
	public function __isset($name)
395
	{
396
		$properties = $this->listObjectProperties();
397
		if (isset($properties[$name])) {
398
			return TRUE;
399
		}
400
401
		if ($name === '') {
402
			return FALSE;
403
		}
404
405
		$methods = $this->listObjectMethods();
406
		$name[0] = $name[0] & "\xDF";
407
408
		return isset($methods['get' . $name]) || isset($methods['is' . $name]);
409
	}
410
411
412
413
	/**
414
	 * Access to undeclared property.
415
	 *
416
	 * @param  string  property name
417
	 * @return void
418
	 * @throws MemberAccessException
419
	 */
420
	public function __unset($name)
421
	{
422
		ObjectMixin::remove($this, $name);
423
	}
424
425
426
427
	/**
428
	 * Should return only public or protected properties of class
429
	 *
430
	 * @return array
431
	 */
432
	private function listObjectProperties()
433
	{
434
		$class = get_class($this);
435
		if (!isset(self::$__properties[$class])) {
436
			$refl = new \ReflectionClass($class);
437
			$properties = array_map(function (\ReflectionProperty $property) {
438
				return $property->getName();
439
			}, $refl->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED));
440
441
			self::$__properties[$class] = array_flip($properties);
442
		}
443
444
		return self::$__properties[$class];
445
	}
446
447
448
449
	/**
450
	 * Should return all public methods of class
451
	 *
452
	 * @return array
453
	 */
454
	private function listObjectMethods()
455
	{
456
		$class = get_class($this);
457
		if (!isset(self::$__methods[$class])) {
458
			$refl = new \ReflectionClass($class);
459
			$methods = array_map(function (\ReflectionMethod $method) {
460
				return $method->getName();
461
			}, $refl->getMethods(\ReflectionMethod::IS_PUBLIC));
462
463
			self::$__methods[$class] = array_flip($methods);
464
		}
465
466
		return self::$__methods[$class];
467
	}
468
469
}
470