Completed
Push — master ( 6ac80e...ea27d1 )
by Chris
03:18
created

Record   F

Complexity

Total Complexity 92

Size/Duplication

Total Lines 785
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 0
loc 785
rs 1.815
c 0
b 0
f 0
wmc 92
lcom 1
cbo 5

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 3
A has() 0 4 2
A get() 0 15 4
A set() 0 10 3
A remove() 0 8 2
A table() 0 13 2
A storage() 0 6 3
A getSharedStorage() 0 4 1
A setSharedStorage() 0 4 1
B prepareData() 0 33 8
A prepareFilter() 0 13 3
A prepareListing() 0 15 3
A load() 0 8 1
A find() 0 14 2
A findOrNew() 0 12 2
A in() 0 4 1
A all() 0 4 1
A eager() 0 13 3
A search() 0 15 3
A listing() 0 9 1
A distinct() 0 11 2
A query() 0 18 2
A saveNew() 0 22 3
A saveExisting() 0 21 3
B save() 0 37 6
A saveMany() 0 12 3
A delete() 0 10 3
A relationAttributes() 0 4 1
A hasRelation() 0 6 1
A relation() 0 21 3
A relations() 0 10 2
A hasRelated() 0 6 2
A related() 0 12 2
A setRelated() 0 16 3
A unsetRelated() 0 8 2
A saveRelations() 0 6 2
A defaultSearchAttributes() 0 4 1
A __call() 0 8 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
namespace Darya\ORM;
3
4
use Exception;
5
use Darya\Storage\Query;
6
use Darya\Storage\Readable;
7
use Darya\Storage\Queryable;
8
use Darya\Storage\Modifiable;
9
use Darya\Storage\Searchable;
10
use Darya\Storage\Aggregational;
11
use Darya\Storage\Query\Builder;
12
13
/**
14
 * Darya's active record implementation.
15
 *
16
 * @author Chris Andrew <[email protected]>
17
 */
18
class Record extends Model
19
{
20
	/**
21
	 * Database table name.
22
	 *
23
	 * Overrides the name of the database table that persists the model. The
24
	 * model's lower-cased class name is used if this is not set.
25
	 *
26
	 * @var string
27
	 */
28
	protected $table;
29
30
	/**
31
	 * Instance storage.
32
	 *
33
	 * @var Readable
34
	 */
35
	protected $storage;
36
37
	/**
38
	 * Shared storage.
39
	 *
40
	 * @var Readable
41
	 */
42
	protected static $sharedStorage;
43
44
	/**
45
	 * Definitions of related models.
46
	 *
47
	 * @var array
48
	 */
49
	protected $relations = array();
50
51
	/**
52
	 * Default searchable attributes.
53
	 *
54
	 * @var array
55
	 */
56
	protected $search = array();
57
58
	/**
59
	 * Instantiate a new record with the given data or load an instance from
60
	 * storage if the given data is a valid primary key.
61
	 *
62
	 * @param mixed $data An array of key-value attributes to set or a primary key to load by
63
	 */
64
	public function __construct($data = null)
65
	{
66
		if (is_numeric($data) || is_string($data)) {
67
			$this->data = static::load($data);
68
		}
69
70
		parent::__construct($data);
71
	}
72
73
	/**
74
	 * Determine whether the given attribute or relation is set on the record.
75
	 *
76
	 * @param string $attribute
77
	 * @return bool
78
	 */
79
	public function has($attribute)
80
	{
81
		return $this->hasRelated($attribute) || parent::has($attribute);
82
	}
83
84
	/**
85
	 * Retrieve the given attribute or relation from the record.
86
	 *
87
	 * @param string $attribute
88
	 * @return mixed
89
	 */
90
	public function get($attribute)
91
	{
92
		list($attribute, $subattribute) = array_pad(explode('.',  $attribute, 2), 2, null);
93
94
		if ($this->hasRelation($attribute)) {
95
			$related = $this->related($attribute);
96
			if ($related instanceof Record && $subattribute !== null) {
97
				return $related->get($subattribute);
98
			}
99
100
			return $related;
101
		}
102
103
		return parent::get($attribute);
104
	}
105
106
	/**
107
	 * Set the value of an attribute or relation on the model.
108
	 *
109
	 * @param string $attribute
110
	 * @param mixed  $value
111
	 */
112
	public function set($attribute, $value = null)
113
	{
114
		if (is_string($attribute) && $this->hasRelation($attribute)) {
115
			$this->setRelated($attribute, $value);
116
117
			return;
118
		}
119
120
		parent::set($attribute, $value);
121
	}
122
123
	/**
124
	 * Unset the value for an attribute or relation on the model.
125
	 *
126
	 * @param string $attribute
127
	 */
128
	public function remove($attribute)
129
	{
130
		if ($this->hasRelation($attribute)) {
131
			return $this->unsetRelated($attribute);
132
		}
133
134
		parent::remove($attribute);
135
	}
136
137
	/**
138
	 * Retrieve the name of the table this model belongs to.
139
	 *
140
	 * If none is set, it defaults to creating it from the class name.
141
	 *
142
	 * For example:
143
	 *     Page        -> pages
144
	 *     PageSection -> page_sections
145
	 *
146
	 * @return string
147
	 */
148
	public function table()
149
	{
150
		if ($this->table) {
151
			return $this->table;
152
		}
153
154
		$split = explode('\\', get_class($this));
155
		$class = end($split);
156
157
		return preg_replace_callback('/([A-Z])/', function ($matches) {
158
			return '_' . strtolower($matches[1]);
159
		}, lcfirst($class)) . 's';
160
	}
161
162
	/**
163
	 * Get and optionally set the model's storage instance.
164
	 *
165
	 * @param Readable $storage [optional]
166
	 * @return Readable
167
	 */
168
	public function storage(Readable $storage = null)
169
	{
170
		$this->storage = $storage ?: $this->storage;
171
172
		return $this->storage ?: static::getSharedStorage();
173
	}
174
175
	/**
176
	 * Get the storage shared to all instances of this model.
177
	 *
178
	 * @return Readable
179
	 */
180
	public static function getSharedStorage()
181
	{
182
		return static::$sharedStorage;
183
	}
184
185
	/**
186
	 * Share the given database connection to all instances of this model.
187
	 *
188
	 * @param Readable $storage
189
	 */
190
	public static function setSharedStorage(Readable $storage)
191
	{
192
		static::$sharedStorage = $storage;
193
	}
194
195
	/**
196
	 * Prepare the record's data for storage.
197
	 *
198
	 * This is here until repositories are implemented.
199
	 *
200
	 * @return array
201
	 */
202
	protected function prepareData()
203
	{
204
		$types = $this->attributes;
205
206
		$changed = array_intersect_key($this->data, array_flip($this->changed));
207
208
		$data = $this->id() ? $changed : $this->data;
209
210
		foreach ($data as $attribute => $value) {
211
			if (isset($types[$attribute])) {
212
				$type = $types[$attribute];
213
214
				switch ($type) {
215
					case 'int':
216
						$value = (int) $value;
217
						break;
218
					case 'date':
219
						$value = date('Y-m-d', $value);
220
						break;
221
					case 'datetime':
222
						$value = date('Y-m-d H:i:s', $value);
223
						break;
224
					case 'time':
225
						$value = date('H:i:s', $value);
226
						break;
227
				}
228
229
				$data[$attribute] = $value;
230
			}
231
		}
232
233
		return $data;
234
	}
235
236
	/**
237
	 * Prepare the given filter.
238
	 *
239
	 * Creates a filter for the record's key attribute if the given value is not
240
	 * an array.
241
	 *
242
	 * TODO: Filter by key if $filter has numeric keys
243
	 *
244
	 * @param mixed $filter
245
	 * @return string
246
	 */
247
	protected static function prepareFilter($filter)
248
	{
249
		if ($filter === null) {
250
			return array();
251
		}
252
253
		if (!is_array($filter)) {
254
			$instance = new static;
255
			$filter = array($instance->key() => $filter);
256
		}
257
258
		return $filter;
259
	}
260
261
	/**
262
	 * Prepare the given list data.
263
	 *
264
	 * @param array  $data
265
	 * @param string $attribute
266
	 * @return array
267
	 */
268
	protected static function prepareListing($data, $attribute)
269
	{
270
		$instance = new static;
271
		$key = $instance->key();
272
273
		$list = array();
274
275
		foreach ($data as $row) {
276
			if (isset($row[$attribute])) {
277
				$list[$row[$key]] = $row[$attribute];
278
			}
279
		}
280
281
		return $list;
282
	}
283
284
	/**
285
	 * Load record data from storage using the given criteria.
286
	 *
287
	 * @param array|string|int $filter [optional]
288
	 * @param array|string     $order  [optional]
289
	 * @param int              $limit  [optional]
290
	 * @param int              $offset [optional]
291
	 * @return array
292
	 */
293
	public static function load($filter = array(), $order = array(), $limit = 0, $offset = 0)
294
	{
295
		$instance = new static;
296
		$storage = $instance->storage();
297
		$filter = static::prepareFilter($filter);
298
299
		return $storage->read($instance->table(), $filter, $order, $limit, $offset);
300
	}
301
302
	/**
303
	 * Load a record instance from storage using the given criteria.
304
	 *
305
	 * Returns false if the record cannot be found.
306
	 *
307
	 * @param array|string|int $filter [optional]
308
	 * @param array|string     $order  [optional]
309
	 * @return Record|bool
310
	 */
311
	public static function find($filter = array(), $order = array())
312
	{
313
		$data = static::load($filter, $order, 1);
314
315
		if (empty($data[0])) {
316
			return false;
317
		}
318
319
		$instance = new static($data[0]);
320
321
		$instance->reinstate();
322
323
		return $instance;
324
	}
325
326
	/**
327
	 * Load a record instance from storage using the given criteria or create a
328
	 * new instance if nothing is found.
329
	 *
330
	 * @param array|string|int $filter [optional]
331
	 * @param array|string     $order  [optional]
332
	 * @return Record
333
	 */
334
	public static function findOrNew($filter = array(), $order = array())
335
	{
336
		$instance = static::find($filter, $order);
337
338
		if ($instance === false) {
339
			return new static;
340
		}
341
342
		$instance->reinstate();
343
344
		return $instance;
345
	}
346
347
	/**
348
	 * Load multiple record instances matching the given IDs.
349
	 *
350
	 * @param array|string|int $ids
351
	 * @return array
352
	 */
353
	public static function in($ids = array())
354
	{
355
		return static::all(['id' => (array) $ids]);
356
	}
357
358
	/**
359
	 * Load multiple record instances from storage using the given criteria.
360
	 *
361
	 * @param array|string|int $filter [optional]
362
	 * @param array|string     $order  [optional]
363
	 * @param int              $limit  [optional]
364
	 * @param int              $offset [optional]
365
	 * @return array
366
	 */
367
	public static function all($filter = array(), $order = array(), $limit = 0, $offset = 0)
368
	{
369
		return static::hydrate(static::load($filter, $order, $limit, $offset));
370
	}
371
372
	/**
373
	 * Eagerly load the given relations of multiple record instances.
374
	 *
375
	 * @param array|string     $relations
376
	 * @param array|string|int $filter    [optional]
377
	 * @param array|string     $order     [optional]
378
	 * @param int              $limit     [optional]
379
	 * @param int              $offset    [optional]
380
	 * @return array
381
	 */
382
	public static function eager($relations, $filter = array(), $order = array(), $limit = 0, $offset = 0)
383
	{
384
		$instance = new static;
385
		$instances = static::all($filter, $order, $limit, $offset);
386
387
		foreach ((array) $relations as $relation) {
388
			if ($instance->relation($relation)) {
389
				$instances = $instance->relation($relation)->eager($instances);
390
			}
391
		}
392
393
		return $instances;
394
	}
395
396
	/**
397
	 * Search for record instances in storage using the given criteria.
398
	 *
399
	 * @param string           $query
400
	 * @param array            $attributes [optional]
401
	 * @param array|string|int $filter     [optional]
402
	 * @param array|string     $order      [optional]
403
	 * @param int              $limit      [optional]
404
	 * @param int              $offset     [optional]
405
	 * @return array
406
	 * @throws Exception
407
	 */
408
	public static function search($query, $attributes = array(), $filter = array(), $order = array(), $limit = null, $offset = 0)
409
	{
410
		$instance = new static;
411
		$storage = $instance->storage();
412
413
		if (!$storage instanceof Searchable) {
414
			throw new Exception(get_class($instance) . ' storage is not searchable');
415
		}
416
417
		$attributes = $attributes ?: $instance->defaultSearchAttributes();
418
419
		$data = $storage->search($instance->table(), $query, $attributes, $filter, $order, $limit, $offset);
420
421
		return static::hydrate($data);
422
	}
423
424
	/**
425
	 * Retrieve key => value pairs using `id` for keys and the given attribute
426
	 * for values.
427
	 *
428
	 * @param string $attribute
429
	 * @param array  $filter    [optional]
430
	 * @param array  $order     [optional]
431
	 * @param int    $limit     [optional]
432
	 * @param int    $offset    [optional]
433
	 * @return array
434
	 */
435
	public static function listing($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0)
436
	{
437
		$instance = new static;
438
		$storage = $instance->storage();
439
440
		$data = $storage->listing($instance->table(), array($instance->key(), $attribute), $filter, $order, $limit, $offset);
441
442
		return static::prepareListing($data, $attribute);
443
	}
444
445
	/**
446
	 * Retrieve the distinct values of the given attribute.
447
	 *
448
	 * @param string $attribute
449
	 * @param array  $filter    [optional]
450
	 * @param array  $order     [optional]
451
	 * @param int    $limit     [optional]
452
	 * @param int    $offset    [optional]
453
	 * @return array
454
	 */
455
	public static function distinct($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0)
456
	{
457
		$instance = new static;
458
		$storage = $instance->storage();
459
460
		if (!$storage instanceof Aggregational) {
461
			return array_values(array_unique(static::listing($attribute, $filter, $order)));
462
		}
463
464
		return $storage->distinct($instance->table(), $attribute, $filter, $order, $limit, $offset);
465
	}
466
467
	/**
468
	 * Create a query builder for the model.
469
	 *
470
	 * @return Builder
471
	 * @throws Exception
472
	 */
473
	public static function query()
474
	{
475
		$instance = new static;
476
		$storage = $instance->storage();
477
478
		if (!$storage instanceof Queryable) {
479
			throw new Exception(get_class($instance) . ' storage is not queryable');
480
		}
481
482
		$query = new Query($instance->table());
483
		$builder = new Builder($query, $storage);
484
485
		$builder->callback(function ($result) use ($instance) {
486
			return $instance::hydrate($result->data);
487
		});
488
489
		return $builder;
490
	}
491
492
	/**
493
	 * Create the model in storage.
494
	 *
495
	 * @return bool
496
	 */
497
	protected function saveNew()
498
	{
499
		$data = $this->prepareData();
500
		$storage = $this->storage();
501
502
		// Create a new item
503
		$id = $storage->create($this->table(), $data);
504
505
		// Bail if saving failed
506
		if (!$id) {
507
			return false;
508
		}
509
510
		// If we didn't get a boolean back, assume this is an ID (TODO: Formalise)
511
		if (!is_bool($id)) {
512
			$this->set($this->key(), $id);
513
		}
514
515
		$this->reinstate();
516
517
		return true;
518
	}
519
520
	/**
521
	 * Update the model in storage.
522
	 *
523
	 * @return bool
524
	 */
525
	protected function saveExisting()
526
	{
527
		$data = $this->prepareData();
528
		$storage = $this->storage();
529
530
		// We can bail if there isn't any new data to save
531
		if (empty($data)) {
532
			return true;
533
		}
534
535
		// Attempt to update an existing item
536
		$updated = $storage->update($this->table(), $data, array($this->key() => $this->id()), 1);
537
538
		// Otherwise it doesn't exist, so we can attempt to create it
539
		// TODO: Query result error check
540
		if (!$updated) {
541
			$updated = $storage->create($this->table(), $data) > 0;
542
		}
543
544
		return $updated;
545
	}
546
547
	/**
548
	 * Save the record to storage.
549
	 *
550
	 * @param array $options [optional]
551
	 * @return bool
552
	 * @throws Exception
553
	 */
554
	public function save(array $options = array())
555
	{
556
		// Bail if the model is not valid
557
		if (!$this->validate()) {
558
			return false;
559
		}
560
561
		$storage = $this->storage();
562
		$class = get_class($this);
563
564
		// Storage must be modifiable in order to save
565
		if (!$storage instanceof Modifiable) {
566
			throw new Exception($class . ' storage is not modifiable');
567
		}
568
569
		// Create or update the model in storage
570
		if (!$this->id()) {
571
			$success = $this->saveNew();
572
		} else {
573
			$success = $this->saveExisting();
574
		}
575
576
		if ($success) {
577
			// Clear the changed attributes; we're in sync now
578
			$this->reinstate();
579
580
			// Save relations if we don't want to skip
581
			if (empty($options['skipRelations'])) {
582
				$this->saveRelations();
583
			}
584
		} else {
585
			$this->errors['save'] = "Failed to save $class instance";
586
			$this->errors['storage'] = $this->storage()->error();
587
		}
588
589
		return $success;
590
	}
591
592
	/**
593
	 * Save multiple record instances to storage.
594
	 *
595
	 * Returns the number of instances that saved successfully.
596
	 *
597
	 * @param array $instances
598
	 * @param array $options   [optional]
599
	 * @return int
600
	 */
601
	public static function saveMany($instances, array $options = array())
602
	{
603
		$saved = 0;
604
605
		foreach ($instances as $instance) {
606
			if ($instance->save($options)) {
607
				$saved++;
608
			}
609
		}
610
611
		return $saved;
612
	}
613
614
	/**
615
	 * Delete the record from storage.
616
	 *
617
	 * @return bool
618
	 */
619
	public function delete()
620
	{
621
		$storage = $this->storage();
622
623
		if (!$this->id() || !($storage instanceof Modifiable)) {
624
			return false;
625
		}
626
627
		return (bool) $storage->delete($this->table(), array($this->key() => $this->id()), 1);
628
	}
629
630
	/**
631
	 * Retrieve the list of relation attributes for this model.
632
	 *
633
	 * @return array
634
	 */
635
	public function relationAttributes()
636
	{
637
		return array_keys($this->relations);
638
	}
639
640
	/**
641
	 * Determine whether the given attribute is a relation.
642
	 *
643
	 * @param string $attribute
644
	 * @return bool
645
	 */
646
	protected function hasRelation($attribute)
647
	{
648
		$attribute = $this->prepareAttribute($attribute);
649
650
		return isset($this->relations[$attribute]);
651
	}
652
653
	/**
654
	 * Retrieve the given relation.
655
	 *
656
	 * @param string $attribute
657
	 * @return Relation
658
	 */
659
	public function relation($attribute)
660
	{
661
		if (!$this->hasRelation($attribute)) {
662
			return null;
663
		}
664
665
		$attribute = $this->prepareAttribute($attribute);
666
		$relation = $this->relations[$attribute];
667
668
		if (!$relation instanceof Relation) {
669
			$type = array_shift($relation);
670
			$arguments = array_merge(array($this), $relation);
671
			$arguments['name'] = $attribute;
672
673
			$relation = Relation::factory($type, $arguments);
674
675
			$this->relations[$attribute] = $relation;
676
		}
677
678
		return $relation;
679
	}
680
681
	/**
682
	 * Retrieve all relations.
683
	 *
684
	 * @return Relation[]
685
	 */
686
	public function relations()
687
	{
688
		$relations = array();
689
690
		foreach ($this->relationAttributes() as $attribute) {
691
			$relations[$attribute] = $this->relation($attribute);
692
		}
693
694
		return $relations;
695
	}
696
697
	/**
698
	 * Determine whether the given relation has any set models.
699
	 *
700
	 * @param string $attribute
701
	 * @return bool
702
	 */
703
	protected function hasRelated($attribute)
704
	{
705
		$attribute = $this->prepareAttribute($attribute);
706
707
		return $this->hasRelation($attribute) && $this->relation($attribute)->count();
708
	}
709
710
	/**
711
	 * Retrieve the models of the given relation.
712
	 *
713
	 * @param string $attribute
714
	 * @return array
715
	 */
716
	public function related($attribute)
717
	{
718
		if (!$this->hasRelation($attribute)) {
719
			return null;
720
		}
721
722
		$attribute = $this->prepareAttribute($attribute);
723
724
		$relation = $this->relation($attribute);
725
726
		return $relation->retrieve();
727
	}
728
729
	/**
730
	 * Set the given related models.
731
	 *
732
	 * @param string $attribute
733
	 * @param mixed  $value
734
	 */
735
	protected function setRelated($attribute, $value)
736
	{
737
		if (!$this->hasRelation($attribute)) {
738
			return;
739
		}
740
741
		$relation = $this->relation($attribute);
742
743
		$relation->detach();
744
745
		if ($value === null) {
746
			return;
747
		}
748
749
		$relation->attach($value);
750
	}
751
752
	/**
753
	 * Unset the models of the given relation.
754
	 *
755
	 * @param string $attribute
756
	 */
757
	protected function unsetRelated($attribute)
758
	{
759
		if (!$this->hasRelation($attribute)) {
760
			return;
761
		}
762
763
		$this->relation($attribute)->detach();
764
	}
765
766
	/**
767
	 * Save all of the model's relations.
768
	 */
769
	public function saveRelations()
770
	{
771
		foreach ($this->relations() as $relation) {
772
			$relation->save();
773
		}
774
	}
775
776
	/**
777
	 * Retrieve the default search attributes for the model.
778
	 *
779
	 * @return array
780
	 */
781
	public function defaultSearchAttributes()
782
	{
783
		return $this->search;
784
	}
785
786
	/**
787
	 * Retrieve a relation. Shortcut for `relation()`.
788
	 *
789
	 * @param string $method
790
	 * @param array  $arguments
791
	 * @return Relation
792
	 * @throws Exception
793
	 */
794
	public function __call($method, $arguments)
795
	{
796
		if ($this->hasRelation($method)) {
797
			return $this->relation($method);
798
		}
799
800
		throw new Exception('Call to undefined method ' . get_class($this) . '::' . $method . '()');
801
	}
802
}
803