Completed
Push — master ( 2d3c47...157dd8 )
by Chris
03:55
created

Record::remove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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