Completed
Push — master ( 2a1c69...ee3e85 )
by Chris
02:41
created

Relation   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 565
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 2

Importance

Changes 26
Bugs 8 Features 6
Metric Value
wmc 65
c 26
b 8
f 6
lcom 3
cbo 2
dl 0
loc 565
rs 5.7894

36 Methods

Rating   Name   Duplication   Size   Complexity  
A arrayify() 0 3 2
A separateKeys() 0 15 3
A resolveClass() 0 7 2
A factory() 0 18 3
A __construct() 0 12 3
A delimitClass() 0 8 1
A prepareForeignKey() 0 3 1
A defaultConstraint() 0 5 1
setDefaultKeys() 0 1 ?
A attributeList() 0 15 4
A reduce() 0 15 4
A replace() 0 21 4
A persist() 0 5 2
A verify() 0 3 1
A verifyModels() 0 11 4
A verifyParents() 0 3 1
A storage() 0 5 4
A name() 0 5 2
A foreignKey() 0 5 2
A localKey() 0 5 2
A constrain() 0 3 1
A constraint() 0 3 1
A filter() 0 3 1
A sort() 0 3 1
A order() 0 3 1
A read() 0 3 1
A load() 0 8 1
A loaded() 0 3 1
eager() 0 1 ?
retrieve() 0 1 ?
A one() 0 7 3
A all() 0 7 2
A count() 0 7 2
A set() 0 5 1
A clear() 0 4 1
A __get() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like Relation 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 Relation, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Darya\ORM;
3
4
use Exception;
5
use ReflectionClass;
6
use Darya\ORM\Record;
7
use Darya\Storage\Readable;
8
9
/**
10
 * Darya's abstract entity relation.
11
 * 
12
 * TODO: errors() method.
13
 * TODO: Filter, order, limit, offset for load() and retrieve().
14
 * TODO: Shouldn't delimitClass() and prepareForeignKey() be static?
15
 * 
16
 * @property-read string   $name
17
 * @property-read Record   $parent
18
 * @property-read Record   $target
19
 * @property-read string   $foreignKey
20
 * @property-read string   $localKey
21
 * @property-read array    $constraint
22
 * @property-read Record[] $related
23
 * @property-read bool     $loaded
24
 * @property-read Readable $storage
25
 * 
26
 * @author Chris Andrew <[email protected]>
27
 */
28
abstract class Relation {
29
	
30
	const HAS             = 'has';
31
	const HAS_MANY        = 'has_many';
32
	const BELONGS_TO      = 'belongs_to';
33
	const BELONGS_TO_MANY = 'belongs_to_many';
34
	
35
	protected static $classMap = array(
36
		self::HAS             => 'Darya\ORM\Relation\Has',
37
		self::HAS_MANY        => 'Darya\ORM\Relation\HasMany',
38
		self::BELONGS_TO      => 'Darya\ORM\Relation\BelongsTo',
39
		self::BELONGS_TO_MANY => 'Darya\ORM\Relation\BelongsToMany',
40
	);
41
	
42
	/**
43
	 * @var string The name of the relation in the context of the parent model
44
	 */
45
	protected $name = '';
46
	
47
	/**
48
	 * @var Record Parent model
49
	 */
50
	protected $parent;
51
	
52
	/**
53
	 * @var Record Target model
54
	 */
55
	protected $target;
56
	
57
	/**
58
	 * @var string Foreign key on the "belongs-to" model
59
	 */
60
	protected $foreignKey;
61
	
62
	/**
63
	 * @var string Local key on the "has" model
64
	 */
65
	protected $localKey;
66
	
67
	/**
68
	 * @var array Filter for constraining related models loaded from storage
69
	 */
70
	protected $constraint = array();
71
	
72
	/**
73
	 * @var array Sort order for related models
74
	 */
75
	protected $sort = array();
76
	
77
	/**
78
	 * @var Record[] The related instances
79
	 */
80
	protected $related = array();
81
	
82
	/**
83
	 * @var bool Determines whether related instances have been loaded
84
	 */
85
	protected $loaded = false;
86
	
87
	/**
88
	 * @var Readable Storage interface
89
	 */
90
	protected $storage;
91
	
92
	/**
93
	 * Helper method for methods that accept single or multiple values, or for
94
	 * just casting to an array without losing a plain object.
95
	 * 
96
	 * Returns a array with the given value as its sole element, if it is not an
97
	 * array already.
98
	 * 
99
	 * @param mixed $value
100
	 * @return array
101
	 */
102
	protected static function arrayify($value) {
103
		return !is_array($value) ? array($value) : $value;
104
	}
105
	
106
	/**
107
	 * Separate array elements with numeric keys from those with string keys.
108
	 * 
109
	 * @param array $array
110
	 * @return array array($numeric, $strings)
111
	 */
112
	protected static function separateKeys(array $array) {
113
		$numeric = array();
114
		$strings = array();
115
		
116
		
117
		foreach ($array as $key => $value) {
118
			if (is_numeric($key)) {
119
				$numeric[$key] = $value;
120
			} else {
121
				$strings[$key] = $value;
122
			}
123
		}
124
		
125
		return array($numeric, $strings);
126
	}
127
	
128
	/**
129
	 * Resolve a relation class name from the given relation type constant.
130
	 * 
131
	 * @param string $type
132
	 * @return string
133
	 */
134
	protected static function resolveClass($type) {
135
		if (isset(static::$classMap[$type])) {
136
			return static::$classMap[$type];
137
		}
138
		
139
		return static::$classMap[static::HAS];
140
	}
141
	
142
	/**
143
	 * Create a new relation of the given type using the given arguments.
144
	 * 
145
	 * Applies numerically-keyed arguments to the constructor and string-keyed
146
	 * arguments to methods with the same name.
147
	 * 
148
	 * @param string $type
149
	 * @param array  $arguments
150
	 * @return Relation
151
	 */
152
	public static function factory($type = self::HAS, array $arguments) {
153
		$class = static::resolveClass($type);
154
		
155
		$reflection = new ReflectionClass($class);
156
		
157
		list($arguments, $named) = static::separateKeys($arguments);
158
		
159
		$instance = $reflection->newInstanceArgs($arguments);
160
		
161
		foreach ($named as $method => $argument) {
162
			if (method_exists($instance, $method)) {
163
				$argument = static::arrayify($argument);
164
				call_user_func_array(array($instance, $method), $argument);
165
			}
166
		}
167
		
168
		return $instance;
169
	}
170
	
171
	/**
172
	 * Instantiate a new relation.
173
	 * 
174
	 * @param Record $parent     Parent class
175
	 * @param string $target     Related class that extends \Darya\ORM\Record
176
	 * @param string $foreignKey [optional] Custom foreign key
177
	 * @param array  $constraint [optional] Constraint filter for related models
178
	 */
179
	public function __construct(Record $parent, $target, $foreignKey = null, array $constraint = array()) {
180
		if (!is_subclass_of($target, 'Darya\ORM\Record')) {
181
			throw new Exception('Target class not does not extend Darya\ORM\Record');
182
		}
183
		
184
		$this->parent = $parent;
185
		$this->target = !is_object($target) ? new $target : $target;
186
		
187
		$this->foreignKey = $foreignKey;
188
		$this->setDefaultKeys();
189
		$this->constrain($constraint);
190
	}
191
	
192
	/**
193
	 * Lowercase and delimit the given PascalCase class name.
194
	 * 
195
	 * @param string $class
196
	 * @return string
197
	 */
198
	protected function delimitClass($class) {
199
		$split = explode('\\', $class);
200
		$class = end($split);
201
		
202
		return preg_replace_callback('/([A-Z])/', function ($matches) {
203
			return '_' . strtolower($matches[1]);
204
		}, lcfirst($class));
205
	}
206
	
207
	/**
208
	 * Prepare a foreign key from the given class name.
209
	 * 
210
	 * @param string $class
211
	 * @return string
212
	 */
213
	protected function prepareForeignKey($class) {
214
		return $this->delimitClass($class) . '_id';
215
	}
216
	
217
	/**
218
	 * Retrieve the default filter for the related models.
219
	 * 
220
	 * @return array
221
	 */
222
	protected function defaultConstraint() {
223
		return array(
224
			$this->foreignKey => $this->parent->id()
225
		);
226
	}
227
	
228
	/**
229
	 * Set the default keys for the relation if they haven't already been set.
230
	 */
231
	abstract protected function setDefaultKeys();
232
	
233
	/**
234
	 * Retrieve the values of the given attribute of the given instances.
235
	 * 
236
	 * Works similarly to array_column(), but doesn't return data from any rows
237
	 * without the given attribute set.
238
	 * 
239
	 * Optionally accepts a second attribute to index by.
240
	 * 
241
	 * @param Record[]|Record|array $instances
242
	 * @param string                $attribute
243
	 * @param string                $index     [optional]
244
	 * @return array
245
	 */
246
	protected static function attributeList($instances, $attribute, $index = null) {
247
		$values = array();
248
		
249
		foreach (static::arrayify($instances) as $instance) {
250
			if (isset($instance[$attribute])) {
251
				if ($index !== null) {
252
					$values[$instance[$index]] = $instance[$attribute];
253
				} else {
254
					$values[] = $instance[$attribute];
255
				}
256
			}
257
		}
258
		
259
		return $values;
260
	}
261
	
262
	/**
263
	 * Reduce the cached related models to those with the given IDs.
264
	 * 
265
	 * If no IDs are given then all of the in-memory models will be removed.
266
	 * 
267
	 * @param int[] $ids
268
	 */
269
	protected function reduce(array $ids = array()) {
270
		if (empty($this->related)) {
271
			return;
272
		}
273
		
274
		$keys = array();
275
		
276
		foreach ($this->related as $key => $instance) {
277
			if (!in_array($instance->id(), $ids)) {
278
				$keys[$key] = null;
279
			}
280
		}
281
		
282
		$this->related = array_values(array_diff_key($this->related, $keys));
283
	}
284
	
285
	/**
286
	 * Replace a cached related model.
287
	 * 
288
	 * If the related model does not have an ID or it is not found, it is simply
289
	 * appended.
290
	 * 
291
	 * Retrieves related models if none have been loaded yet.
292
	 * 
293
	 * @param Record $instance
294
	 */
295
	protected function replace(Record $instance) {
296
		$this->verify($instance);
297
		
298
		$this->retrieve();
299
		
300
		if (!$instance->id()) {
301
			$this->related[] = $instance;
302
			
303
			return;
304
		}
305
		
306
		foreach ($this->related as $key => $related) {
307
			if ($related->id() === $instance->id()) {
308
				$this->related[$key] = $instance;
309
				
310
				return;
311
			}
312
		}
313
		
314
		$this->related[] = $instance;
315
	}
316
	
317
	/**
318
	 * Save the given record to storage if it hasn't got an ID.
319
	 * 
320
	 * @param Record $instance
321
	 */
322
	protected function persist(Record $instance) {
323
		if (!$instance->id()) {
324
			$instance->save();
325
		}
326
	}
327
	
328
	/**
329
	 * Verify that the given models are instances of the relation's target
330
	 * class.
331
	 * 
332
	 * Throws an exception if any of them aren't.
333
	 * 
334
	 * @param Record[]|Record $instances
335
	 * @throws Exception
336
	 */
337
	protected function verify($instances) {
338
		static::verifyModels($instances, get_class($this->target));
339
	}
340
	
341
	/**
342
	 * Verify that the given objects are instances of the given class.
343
	 * 
344
	 * @param object[]|object $instances
345
	 * @param string          $class
346
	 * @throws Exception
347
	 */
348
	protected static function verifyModels($instances, $class) {
349
		if (!class_exists($class)) {
350
			return;
351
		}
352
		
353
		foreach (static::arrayify($instances) as $instance) {
354
			if (!$instance instanceof $class) {
355
				throw new Exception('Related models must be an instance of ' . $class);
356
			}
357
		}
358
	}
359
	
360
	/**
361
	 * Verify that the given models are instances of the relation's parent
362
	 * class.
363
	 * 
364
	 * Throws an exception if any of them aren't.
365
	 * 
366
	 * @param Record[]|Record $instances
367
	 * @throws Exception
368
	 */
369
	protected function verifyParents($instances) {
370
		static::verifyModels($instances, get_class($this->parent));
371
	}
372
	
373
	/**
374
	 * Retrieve and optionally set the storage used for the target model.
375
	 * 
376
	 * Falls back to target model storage, then parent model storage.
377
	 * 
378
	 * @param Readable $storage
379
	 */
380
	public function storage(Readable $storage = null) {
381
		$this->storage = $storage ?: $this->storage;
382
		
383
		return $this->storage ?: $this->target->storage() ?: $this->parent->storage();
384
	}
385
	
386
	/**
387
	 * Retrieve and optionally set the name of the relation on the parent model.
388
	 * 
389
	 * @param string $name [optional]
390
	 * @return string
391
	 */
392
	public function name($name = '') {
393
		$this->name = (string) $name ?: $this->name;
394
		
395
		return $this->name;
396
	}
397
	
398
	/**
399
	 * Retrieve and optionally set the foreign key for the "belongs-to" model.
400
	 * 
401
	 * @param string $foreignKey [optional]
402
	 * @return string
403
	 */
404
	public function foreignKey($foreignKey = '') {
405
		$this->foreignKey = (string) $foreignKey ?: $this->foreignKey;
406
		
407
		return $this->foreignKey;
408
	}
409
	
410
	/**
411
	 * Retrieve and optionally set the local key for the "has" model.
412
	 * 
413
	 * @param string $localKey [optional]
414
	 * @return string
415
	 */
416
	public function localKey($localKey = '') {
417
		$this->localKey = (string) $localKey ?: $this->localKey;
418
		
419
		return $this->localKey;
420
	}
421
	
422
	/**
423
	 * Set a filter to constrain which models are considered related.
424
	 * 
425
	 * @param array $filter
426
	 */
427
	public function constrain(array $filter) {
428
		$this->constraint = $filter;
429
	}
430
	
431
	/**
432
	 * Retrieve the custom filter used to constrain related models.
433
	 * 
434
	 * @return array
435
	 */
436
	public function constraint() {
437
		return $this->constraint;
438
	}
439
	
440
	/**
441
	 * Retrieve the filter for this relation.
442
	 * 
443
	 * @return array
444
	 */
445
	public function filter() {
446
		return array_merge($this->defaultConstraint(), $this->constraint());
447
	}
448
	
449
	/**
450
	 * Set the sorting order for this relation.
451
	 * 
452
	 * @param array|string $order
453
	 */
454
	public function sort($order) {
455
		return $this->sort = $order;
0 ignored issues
show
Documentation Bug introduced by
It seems like $order can also be of type string. However, the property $sort is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
456
	}
457
	
458
	/**
459
	 * Retrieve the order for this relation.
460
	 * 
461
	 * @return array|string
462
	 */
463
	public function order() {
464
		return $this->sort;
465
	}
466
	
467
	/**
468
	 * Read related model data from storage.
469
	 * 
470
	 * TODO: $filter, $order, $offset
471
	 * 
472
	 * @param int $limit [optional]
473
	 * @return array
474
	 */
475
	public function read($limit = 0) {
476
		return $this->storage()->read($this->target->table(), $this->filter(), $this->order(), $limit);
477
	}
478
	
479
	/**
480
	 * Read, generate and set cached related models from storage.
481
	 * 
482
	 * @param int $limit [optional]
483
	 * @return Record[]
484
	 */
485
	public function load($limit = 0) {
486
		$data = $this->read($limit);
487
		$class = get_class($this->target);
488
		$this->related = $class::generate($data);
489
		$this->loaded = true;
490
		
491
		return $this->related;
492
	}
493
	
494
	/**
495
	 * Determine whether cached related models have been loaded.
496
	 * 
497
	 * @return bool
498
	 */
499
	public function loaded() {
500
		return $this->loaded;
501
	}
502
	
503
	/**
504
	 * Eagerly load the related models for the given parent instances.
505
	 * 
506
	 * Returns the given instances with their related models loaded.
507
	 * 
508
	 * @param array $instances
509
	 * @return array
510
	 */
511
	abstract public function eager(array $instances);
512
	
513
	/**
514
	 * Retrieve one or many related model instances, depending on the relation.
515
	 * 
516
	 * @return Record[]|Record|null
517
	 */
518
	abstract public function retrieve();
519
	
520
	/**
521
	 * Retrieve one related model instance.
522
	 * 
523
	 * @return Record|null
524
	 */
525
	public function one() {
526
		if (!$this->loaded()) {
527
			$this->load(1);
528
		}
529
		
530
		return !empty($this->related) ? $this->related[0] : null;
531
	}
532
	
533
	/**
534
	 * Retrieve all related model instances.
535
	 * 
536
	 * @return Record[]|null
537
	 */
538
	public function all() {
539
		if (!$this->loaded()) {
540
			$this->load();
541
		}
542
		
543
		return $this->related;
544
	}
545
	
546
	/**
547
	 * Count the number of related model instances.
548
	 * 
549
	 * Counts loaded instances if they are present, queries storage otherwise.
550
	 * 
551
	 * @return int
552
	 */
553
	public function count() {
554
		if (!$this->loaded()) {
555
			return $this->storage()->count($this->target->table(), $this->filter());
556
		}
557
		
558
		return count($this->related);
559
	}
560
	
561
	/**
562
	 * Set the related models.
563
	 * 
564
	 * @param Record[] $instances
565
	 */
566
	public function set($instances) {
567
		$this->verify($instances);
568
		$this->related = static::arrayify($instances);
569
		$this->loaded = true;
570
	}
571
	
572
	/**
573
	 * Clear the related models.
574
	 */
575
	public function clear() {
576
		$this->related = array();
577
		$this->loaded = false;
578
	}
579
	
580
	/**
581
	 * Read-only access for relation properties.
582
	 * 
583
	 * @param string $property
584
	 * @return mixed
585
	 */
586
	public function __get($property) {
587
		if (property_exists($this, $property)) {
588
			return $this->$property;
589
		}
590
	}
591
	
592
}
593