Completed
Push — master ( a9493f...dd5657 )
by Chris
03:27
created

Relation   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 15
Bugs 5 Features 3
Metric Value
wmc 55
c 15
b 5
f 3
lcom 1
cbo 2
dl 0
loc 455
rs 6.8

29 Methods

Rating   Name   Duplication   Size   Complexity  
A factory() 0 19 4
A __construct() 0 12 4
A delimitClass() 0 5 1
A prepareForeignKey() 0 3 1
A defaultConstraints() 0 5 1
setDefaultKeys() 0 1 ?
A arrayify() 0 3 2
A attributeList() 0 15 4
A reduce() 0 15 4
B replace() 0 29 5
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 constrain() 0 3 1
A constraints() 0 3 1
A filter() 0 3 1
A read() 0 3 1
A load() 0 7 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 4 1
A clear() 0 3 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
 * TODO: Use a $loaded property instead of setting relations to null.
16
 * 
17
 * @author Chris Andrew <[email protected]>
18
 */
19
abstract class Relation {
20
	
21
	const HAS             = 'has';
22
	const HAS_MANY        = 'has_many';
23
	const BELONGS_TO      = 'belongs_to';
24
	const BELONGS_TO_MANY = 'belongs_to_many';
25
	
26
	/**
27
	 * @var string The name of the relation in the context of the parent model
28
	 */
29
	protected $name;
30
	
31
	/**
32
	 * @var Record Parent model
33
	 */
34
	protected $parent;
35
	
36
	/**
37
	 * @var Record Target model
38
	 */
39
	protected $target;
40
	
41
	/**
42
	 * @var string Foreign key on the "belongs-to" model
43
	 */
44
	protected $foreignKey;
45
	
46
	/**
47
	 * @var string Local key on the "has" model
48
	 */
49
	protected $localKey;
50
	
51
	/**
52
	 * @var array Filters for constraining related models loaded from storage
53
	 */
54
	protected $constraints = array();
55
	
56
	/**
57
	 * @var Record[]|null The related instances
58
	 */
59
	protected $related = null;
60
	
61
	/**
62
	 * @var Readable Storage interface
63
	 */
64
	protected $storage;
65
	
66
	/**
67
	 * Create a new relation of the given type using the given arguments.
68
	 * 
69
	 * @param string $type
70
	 * @param array  $arguments
71
	 * @return Relation
72
	 */
73
	public static function factory($type = self::HAS, array $arguments) {
74
		switch ($type) {
75
			case static::HAS_MANY:
76
				$class = 'Darya\ORM\Relation\HasMany';
77
				break;
78
			case static::BELONGS_TO:
79
				$class = 'Darya\ORM\Relation\BelongsTo';
80
				break;
81
			case static::BELONGS_TO_MANY:
82
				$class = 'Darya\ORM\Relation\BelongsToMany';
83
				break;
84
			default:
85
				$class = 'Darya\ORM\Relation\Has';
86
		}
87
		
88
		$reflection = new ReflectionClass($class);
89
		
90
		return $reflection->newInstanceArgs($arguments);
91
	}
92
	
93
	/**
94
	 * Instantiate a new relation.
95
	 * 
96
	 * @param Record $parent     Parent class
97
	 * @param string $target     Related class that extends \Darya\ORM\Record
98
	 * @param string $foreignKey Custom foreign key
99
	 * @param array  $constraint Constraint filter for related models
100
	 */
101
	public function __construct(Record $parent, $target, $foreignKey = null, $constraint = null) {
102
		if (!is_subclass_of($target, 'Darya\ORM\Record')) {
103
			throw new Exception('Target class not does not extend Darya\ORM\Record');
104
		}
105
		
106
		$this->parent = $parent;
107
		$this->target = !is_object($target) ? new $target : $target;
108
		
109
		$this->foreignKey = $foreignKey;
110
		$this->setDefaultKeys();
111
		$this->constrain($constraint ?: array());
112
	}
113
	
114
	/**
115
	 * Lowercase and delimit the given PascalCase class name.
116
	 * 
117
	 * @param string $class
118
	 * @return string
119
	 */
120
	protected function delimitClass($class) {
121
		return preg_replace_callback('/([A-Z])/', function ($matches) {
122
			return '_' . strtolower($matches[1]);
123
		}, lcfirst(basename($class)));
124
	}
125
	
126
	/**
127
	 * Prepare a foreign key from the given class name.
128
	 * 
129
	 * @param string $class
130
	 * @return string
131
	 */
132
	protected function prepareForeignKey($class) {
133
		return $this->delimitClass($class) . '_id';
134
	}
135
	
136
	/**
137
	 * Retrieve the default filter for this relation.
138
	 * 
139
	 * @return array
140
	 */
141
	protected function defaultConstraints() {
142
		return array(
143
			$this->foreignKey => $this->parent->id()
144
		);
145
	}
146
	
147
	/**
148
	 * Set the default keys for the relation if they haven't already been set.
149
	 */
150
	abstract protected function setDefaultKeys();
151
	
152
	/**
153
	 * Helper method for methods that accept single or multiple values.
154
	 * 
155
	 * Returns a array with the given value as its sole element, if it is not an
156
	 * array already.
157
	 * 
158
	 * @param mixed $value
159
	 * @return array
160
	 */
161
	protected static function arrayify($value) {
162
		return !is_array($value) ? array($value) : $value;
163
	}
164
	
165
	/**
166
	 * Retrieve the values of the given attribute of the given instances.
167
	 * 
168
	 * Works similarly to array_column(), but doesn't return data from any rows
169
	 * without the given attribute set.
170
	 * 
171
	 * @param Record[]|Record $instances
172
	 * @param string $attribute
173
	 * @return array
174
	 */
175
	protected static function attributeList($instances, $attribute, $index = null) {
176
		$values = array();
177
		
178
		foreach (static::arrayify($instances) as $instance) {
179
			if (isset($instance[$attribute])) {
180
				if ($index !== null) {
181
					$values[$instance[$index]] = $instance[$attribute];
182
				} else {
183
					$values[] = $instance[$attribute];
184
				}
185
			}
186
		}
187
		
188
		return $values;
189
	}
190
	
191
	/**
192
	 * Reduce the cached related models to those with the given IDs.
193
	 * 
194
	 * If no IDs are given then all of the in-memory models will be removed.
195
	 * 
196
	 * @param int[] $ids
197
	 */
198
	protected function reduce(array $ids = array()) {
199
		if (empty($this->related)) {
200
			return;
201
		}
202
		
203
		$keys = array();
204
		
205
		foreach ($this->related as $key => $instance) {
206
			if (!in_array($instance->id(), $ids)) {
207
				$keys[$key] = null;
208
			}
209
		}
210
		
211
		$this->related = array_values(array_diff_key($this->related, $keys));
212
	}
213
	
214
	/**
215
	 * Replace a cached related model.
216
	 * 
217
	 * If the related model does not have an ID or it is not found, it is simply
218
	 * appended.
219
	 * 
220
	 * Retrieves related models if none have been loaded yet.
221
	 * 
222
	 * @param Record $instance
223
	 */
224
	protected function replace(Record $instance) {
225
		$this->verify($instance);
226
		
227
		$this->retrieve();
228
		
229
		if (!$instance->id()) {
230
			$this->related[] = $instance;
231
			
232
			return;
233
		}
234
		
235
		$replace = null;
236
		
237
		foreach ($this->related as $key => $related) {
0 ignored issues
show
Bug introduced by
The expression $this->related of type array<integer,object<Darya\ORM\Record>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
238
			if ($related->id() === $instance->id()) {
239
				$replace = $key;
240
				
241
				break;
242
			}
243
		}
244
		
245
		if ($replace === null) {
246
			$this->related[] = $instance;
247
			
248
			return;
249
		}
250
		
251
		$this->related[$replace] = $instance;
252
	}
253
	
254
	/**
255
	 * Save the given record to storage if it hasn't got an ID.
256
	 * 
257
	 * @param Record $instance
258
	 */
259
	protected function persist(Record $instance) {
260
		if (!$instance->id()) {
261
			$instance->save();
262
		}
263
	}
264
	
265
	/**
266
	 * Verify that the given models are instances of the relation's target
267
	 * class.
268
	 * 
269
	 * Throws an exception if any of them aren't.
270
	 * 
271
	 * @param Record[]|Record $instances
272
	 * @throws Exception
273
	 */
274
	protected function verify($instances) {
275
		static::verifyModels($instances, get_class($this->target));
276
	}
277
	
278
	/**
279
	 * Verify that the given objects are instances of the given class.
280
	 * 
281
	 * @param object[]|object $instances
282
	 * @param string          $class
283
	 * @throws Exception
284
	 */
285
	protected static function verifyModels($instances, $class) {
286
		if (!class_exists($class)) {
287
			return;
288
		}
289
		
290
		foreach (static::arrayify($instances) as $instance) {
291
			if (!$instance instanceof $class) {
292
				throw new Exception('Related models must be an instance of ' . $class);
293
			}
294
		}
295
	}
296
	
297
	/**
298
	 * Verify that the given models are instances of the relation's parent
299
	 * class.
300
	 * 
301
	 * Throws an exception if any of them aren't.
302
	 * 
303
	 * @param Record[]|Record $instances
304
	 * @throws Exception
305
	 */
306
	protected function verifyParents($instances) {
307
		static::verifyModels($instances, get_class($this->parent));
308
	}
309
	
310
	/**
311
	 * Retrieve and optionally set the storage used for the target model.
312
	 * 
313
	 * Falls back to target model storage, then parent model storage.
314
	 * 
315
	 * @param Readable $storage
316
	 */
317
	public function storage(Readable $storage = null) {
318
		$this->storage = $storage ?: $this->storage;
319
		
320
		return $this->storage ?: $this->target->storage() ?: $this->parent->storage();
321
	}
322
	
323
	/**
324
	 * Set a filter to constrain which models are considered related.
325
	 * 
326
	 * @param array $filter
327
	 */
328
	public function constrain(array $filter) {
329
		$this->constraints = $filter;
330
	}
331
	
332
	/**
333
	 * Retrieve the custom filters used to constrain related models.
334
	 * 
335
	 * @return array
336
	 */
337
	public function constraints() {
338
		return $this->constraints;
339
	}
340
	
341
	/**
342
	 * Retrieve the filter for this relation.
343
	 * 
344
	 * @return array
345
	 */
346
	public function filter() {
347
		return array_merge($this->defaultConstraints(), $this->constraints());
348
	}
349
	
350
	/**
351
	 * Read related model data from storage.
352
	 * 
353
	 * TODO: $filter, $order, $offset
354
	 * 
355
	 * @param int $limit [optional]
356
	 * @return array
357
	 */
358
	public function read($limit = 0) {
359
		return $this->storage()->read($this->target->table(), $this->filter(), null, $limit);
360
	}
361
	
362
	/**
363
	 * Read, generate and set cached related models from storage.
364
	 * 
365
	 * @param int $limit [optional]
366
	 * @return Record[]
367
	 */
368
	public function load($limit = 0) {
369
		$data = $this->read($limit);
370
		$class = get_class($this->target);
371
		$this->related = $class::generate($data);
372
		
373
		return $this->related;
374
	}
375
	
376
	/**
377
	 * Determine whether cached related models have been attempted to be loaded.
378
	 * 
379
	 * @return bool
380
	 */
381
	public function loaded() {
382
		return $this->related !== null;
383
	}
384
	
385
	/**
386
	 * Eagerly load the related models for the given parent instances.
387
	 * 
388
	 * Returns the given instances with their related models loaded.
389
	 * 
390
	 * @param array $instances
391
	 * @param string $name TODO: Remove this and store as a property
392
	 * @return array
393
	 */
394
	abstract public function eager(array $instances, $name);
395
	
396
	/**
397
	 * Retrieve one or many related model instances, depending on the relation.
398
	 * 
399
	 * @return Record[]|Record|null
400
	 */
401
	abstract public function retrieve();
402
	
403
	/**
404
	 * Retrieve one related model instance.
405
	 * 
406
	 * @return Record|null
407
	 */
408
	public function one() {
409
		if (!$this->loaded()) {
410
			$this->load(1);
411
		}
412
		
413
		return !empty($this->related) ? $this->related[0] : null;
414
	}
415
	
416
	/**
417
	 * Retrieve all related model instances.
418
	 * 
419
	 * @return Record[]|null
420
	 */
421
	public function all() {
422
		if (!$this->loaded()) {
423
			$this->load();
424
		}
425
		
426
		return $this->related;
427
	}
428
	
429
	/**
430
	 * Count the number of related model instances.
431
	 * 
432
	 * Counts loaded instances if they are present, queries storage otherwise.
433
	 * 
434
	 * @return int
435
	 */
436
	public function count() {
437
		if (!$this->loaded()) {
438
			return $this->storage()->count($this->target->table(), $this->filter());
439
		}
440
		
441
		return count($this->related);
442
	}
443
	
444
	/**
445
	 * Set the related models.
446
	 * 
447
	 * @param Record[] $instances
448
	 */
449
	public function set($instances) {
450
		$this->verify($instances);
451
		$this->related = static::arrayify($instances);
452
	}
453
	
454
	/**
455
	 * Clear the related models.
456
	 */
457
	public function clear() {
458
		$this->related = null;
459
	}
460
	
461
	/**
462
	 * Read-only access for relation properties.
463
	 * 
464
	 * @param string $property
465
	 * @return mixed
466
	 */
467
	public function __get($property) {
468
		if (property_exists($this, $property)) {
469
			return $this->$property;
470
		}
471
	}
472
	
473
}
474