Completed
Pull Request — master (#228)
by Lukáš
14:44 queued 05:31
created

MagicAccessors   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 3
Bugs 2 Features 0
Metric Value
wmc 81
c 3
b 2
f 0
lcom 1
cbo 5
dl 0
loc 451
rs 4.8718

13 Methods

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