Completed
Push — master ( 1ecd92...72ecb6 )
by Chris
02:43
created

Record::prepareData()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 22
nc 14
nop 0
dl 0
loc 33
rs 5.3846
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
	 * Overrides the name of the database table that persists the model. The
24
	 * model's lowercased class name is used if this is not set.
25
	 * 
26
	 * @var string Database table name
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
	 */
78
	public function has($attribute)
79
	{
80
		return $this->hasRelated($attribute) || parent::has($attribute);
81
	}
82
	
83
	/**
84
	 * Retrieve the given attribute or relation from the record.
85
	 * 
86
	 * @param string $attribute
87
	 * @return mixed
88
	 */
89
	public function get($attribute)
90
	{
91
		list($attribute, $subattribute) = array_pad(explode('.',  $attribute, 2), 2, null);
92
		
93
		if ($this->hasRelation($attribute)) {
94
			$related = $this->related($attribute);
95
			
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
			return $this->setRelated($attribute, $value);
116
		}
117
		
118
		parent::set($attribute, $value);
119
	}
120
	
121
	/**
122
	 * Retrieve the name of the table this model belongs to.
123
	 * 
124
	 * If none is set, it defaults to creating it from the class name.
125
	 * 
126
	 * For example:
127
	 *     Page        -> pages
128
	 *     PageSection -> page_sections
129
	 * 
130
	 * @return string
131
	 */
132
	public function table()
133
	{
134
		if ($this->table) {
135
			return $this->table;
136
		}
137
		
138
		$split = explode('\\', get_class($this));
139
		$class = end($split);
140
		
141
		return preg_replace_callback('/([A-Z])/', function ($matches) {
142
			return '_' . strtolower($matches[1]);
143
		}, lcfirst($class)) . 's';
144
	}
145
	
146
	/**
147
	 * Get and optionally set the model's storage instance.
148
	 * 
149
	 * @return Readable
150
	 */
151
	public function storage(Readable $storage = null)
152
	{
153
		$this->storage = $storage ?: $this->storage;
154
		
155
		return $this->storage ?: static::getSharedStorage();
156
	}
157
	
158
	/**
159
	 * Get the storage shared to all instances of this model.
160
	 * 
161
	 * @return Readable
162
	 */
163
	public static function getSharedStorage()
164
	{
165
		return static::$sharedStorage;
166
	}
167
	
168
	/**
169
	 * Share the given database connection to all instances of this model.
170
	 * 
171
	 * @param Readable $storage
172
	 */
173
	public static function setSharedStorage(Readable $storage)
174
	{
175
		static::$sharedStorage = $storage;
176
	}
177
	
178
	/**
179
	 * Prepare the record's data for storage. This is here until repositories
180
	 * are implemented.
181
	 * 
182
	 * @return array
183
	 */
184
	protected function prepareData()
185
	{
186
		$types = $this->attributes;
187
		
188
		$changed = array_intersect_key($this->data, array_flip($this->changed));
189
		
190
		$data = $this->id() ? $changed : $this->data;
191
		
192
		foreach ($data as $attribute => $value) {
193
			if (isset($types[$attribute])) {
194
				$type = $types[$attribute];
195
				
196
				switch ($type) {
197
					case 'int':
198
						$value = (int) $value;
199
						break;
200
					case 'date':
201
						$value = date('Y-m-d', $value);
202
						break;
203
					case 'datetime':
204
						$value = date('Y-m-d H:i:s', $value);
205
						break;
206
					case 'time':
207
						$value = date('H:i:s', $value);
208
						break;
209
				}
210
				
211
				$data[$attribute] = $value;
212
			}
213
		}
214
		
215
		return $data;
216
	}
217
	
218
	/**
219
	 * Prepare the given filter.
220
	 * 
221
	 * Creates a filter for the record's key attribute if the given value is not
222
	 * an array.
223
	 * 
224
	 * @param mixed $filter
225
	 * @return string
226
	 */
227
	protected static function prepareFilter($filter)
228
	{
229
		if ($filter === null) {
230
			return array();
231
		}
232
		
233
		if (!is_array($filter)) {
234
			$instance = new static;
235
			$filter = array($instance->key() => $filter);
236
		}
237
		
238
		return $filter;
239
	}
240
	
241
	/**
242
	 * Prepare the given list data.
243
	 * 
244
	 * @param array  $data
245
	 * @param string $attribute
246
	 * @return array
247
	 */
248
	protected static function prepareListing($data, $attribute)
249
	{
250
		$instance = new static;
251
		$key = $instance->key();
252
		
253
		$list = array();
254
		
255
		foreach ($data as $row) {
256
			if (isset($row[$attribute])) {
257
				$list[$row[$key]] = $row[$attribute];
258
			}
259
		}
260
		
261
		return $list;
262
	}
263
	
264
	/**
265
	 * Load record data from storage using the given criteria.
266
	 * 
267
	 * @param array|string|int $filter [optional]
268
	 * @param array|string     $order  [optional]
269
	 * @param int              $limit  [optional]
270
	 * @param int              $offset [optional]
271
	 * @return array
272
	 */
273
	public static function load($filter = array(), $order = array(), $limit = 0, $offset = 0)
274
	{
275
		$instance = new static;
276
		$storage = $instance->storage();
277
		$filter = static::prepareFilter($filter);
278
		
279
		return $storage->read($instance->table(), $filter, $order, $limit, $offset);
280
	}
281
	
282
	/**
283
	 * Load a record instance from storage using the given criteria.
284
	 * 
285
	 * Returns false if the record cannot be found.
286
	 * 
287
	 * @param array|string|int $filter [optional]
288
	 * @param array|string     $order  [optional]
289
	 * @return Record|bool
290
	 */
291
	public static function find($filter = array(), $order = array())
292
	{
293
		$data = static::load($filter, $order, 1);
294
		
295
		if (empty($data[0])) {
296
			return false;
297
		}
298
		
299
		$instance = new static($data[0]);
300
		
301
		$instance->reinstate();
302
		
303
		return $instance;
304
	}
305
	
306
	/**
307
	 * Load a record instance from storage using the given criteria or create a
308
	 * new instance if nothing is found.
309
	 * 
310
	 * @param array|string|int $filter [optional]
311
	 * @param array|string     $order  [optional]
312
	 * @return Record
313
	 */
314
	public static function findOrNew($filter = array(), $order = array())
315
	{
316
		$instance = static::find($filter, $order);
317
		
318
		if ($instance === false) {
319
			return new static;
320
		}
321
		
322
		$instance->reinstate();
323
		
324
		return $instance;
325
	}
326
327
	/**
328
	 * Load multiple record instances from storage using the given criteria.
329
	 * 
330
	 * @param array|string|int $filter [optional]
331
	 * @param array|string     $order  [optional]
332
	 * @param int              $limit  [optional]
333
	 * @param int              $offset [optional]
334
	 * @return array
335
	 */
336
	public static function all($filter = array(), $order = array(), $limit = 0, $offset = 0)
337
	{
338
		return static::hydrate(static::load($filter, $order, $limit, $offset));
339
	}
340
	
341
	/**
342
	 * Eagerly load the given relations of multiple record instances.
343
	 * 
344
	 * @param array|string     $relations
345
	 * @param array|string|int $filter    [optional]
346
	 * @param array|string     $order     [optional]
347
	 * @param int              $limit     [optional]
348
	 * @param int              $offset    [optional]
349
	 * @return array
350
	 */
351
	public static function eager($relations, $filter = array(), $order = array(), $limit = 0, $offset = 0)
352
	{
353
		$instance = new static;
354
		$instances = static::all($filter, $order, $limit, $offset);
355
		
356
		foreach ((array) $relations as $relation) {
357
			if ($instance->relation($relation)) {
358
				$instances = $instance->relation($relation)->eager($instances);
359
			}
360
		}
361
		
362
		return $instances;
363
	}
364
	
365
	/**
366
	 * Search for record instances in storage using the given criteria.
367
	 * 
368
	 * @param string           $query
369
	 * @param array            $attributes [optional]
370
	 * @param array|string|int $filter     [optional]
371
	 * @param array|string     $order      [optional]
372
	 * @param int              $limit      [optional]
373
	 * @param int              $offset     [optional]
374
	 * @return array
375
	 */
376
	public static function search($query, $attributes = array(), $filter = array(), $order = array(), $limit = null, $offset = 0)
377
	{
378
		$instance = new static;
379
		$storage = $instance->storage();
380
		
381
		if (!$storage instanceof Searchable) {
382
			throw new Exception(get_class($instance) . ' storage is not searchable');
383
		}
384
		
385
		$attributes = $attributes ?: $instance->defaultSearchAttributes();
386
		
387
		$data = $storage->search($instance->table(), $query, $attributes, $filter, $order, $limit, $offset);
388
		
389
		return static::hydrate($data);
390
	}
391
	
392
	/**
393
	 * Retrieve key => value pairs using `id` for keys and the given attribute
394
	 * for values.
395
	 * 
396
	 * @param string $attribute
397
	 * @param array  $filter    [optional]
398
	 * @param array  $order     [optional]
399
	 * @param int    $limit     [optional]
400
	 * @param int    $offset    [optional]
401
	 * @return array
402
	 */
403
	public static function listing($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0)
404
	{
405
		$instance = new static;
406
		$storage = $instance->storage();
407
		
408
		$data = $storage->listing($instance->table(), array($instance->key(), $attribute), $filter, $order, $limit, $offset);
409
		
410
		return static::prepareListing($data, $attribute);
411
	}
412
	
413
	/**
414
	 * Retrieve the distinct values of the given attribute.
415
	 * 
416
	 * @param string $attribute
417
	 * @param array  $filter    [optional]
418
	 * @param array  $order     [optional]
419
	 * @param int    $limit     [optional]
420
	 * @param int    $offset    [optional]
421
	 * @return array
422
	 */
423
	public static function distinct($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0)
424
	{
425
		$instance = new static;
426
		$storage = $instance->storage();
427
		
428
		if (!$storage instanceof Aggregational) {
429
			return array_values(array_unique(static::listing($attribute, $filter, $order)));
430
		}
431
		
432
		return $storage->distinct($instance->table(), $attribute, $filter, $order, $limit, $offset);
433
	}
434
	
435
	/**
436
	 * Create a query builder for the model.
437
	 * 
438
	 * @return Builder
439
	 */
440
	public static function query()
441
	{
442
		$instance = new static;
443
		$storage = $instance->storage();
444
		
445
		if (!$storage instanceof Queryable) {
446
			throw new Exception(get_class($instance) . ' storage is not queryable');
447
		}
448
		
449
		$query = new Query($instance->table());
450
		$builder = new Builder($query, $storage);
451
		
452
		$builder->callback(function ($result) use ($instance) {
453
			return $instance::hydrate($result->data);
454
		});
455
		
456
		return $builder;
457
	}
458
	
459
	/**
460
	 * Save the record to storage.
461
	 * 
462
	 * @return bool
463
	 */
464
	public function save()
465
	{
466
		// Bail if the model is not valid
467
		if (!$this->validate()) {
468
			return false;
469
		}
470
		
471
		$storage = $this->storage();
472
		$class = get_class($this);
473
		
474
		// Storage must be modifiable in order to save
475
		if (!$storage instanceof Modifiable) {
476
			throw new Exception($class . ' storage is not modifiable');
477
		}
478
		
479
		$data = $this->prepareData();
480
		
481
		// Bail if there is no data to save
482
		if (empty($data)) {
483
			return true;
484
		}
485
		
486
		if (!$this->id()) {
487
			// Create a new item if there is no ID
488
			$id = $storage->create($this->table(), $data);
489
			
490
			// If we get a new ID, it saved successfully, so let's update the
491
			// model and clear its changes
492
			if ($id) {
493
				$this->set($this->key(), $id);
494
				$this->reinstate();
495
				
496
				return true;
497
			}
498
		} else {
499
			// Attempt to update an existing item if there is an ID
500
			$updated = $storage->update($this->table(), $data, array($this->key() => $this->id()), 1);
501
			
502
			// Otherwise it probably doesn't exist, so we can attempt to create
503
			// TODO: Query result error check
504
			if (!$updated) {
505
				$updated = $storage->create($this->table(), $data) > 0;
506
			}
507
			
508
			// If it updated successfully we can clear model's changes
509
			if ($updated) {
510
				$this->reinstate();
511
				
512
				return true;
513
			}
514
		}
515
		
516
		$this->errors['save'] = "Failed to save $class instance";
517
		$this->errors['storage'] = $this->storage()->error();
518
		
519
		return false;
520
	}
521
	
522
	/**
523
	 * Save multiple record instances to storage.
524
	 * 
525
	 * Returns the number of instances that saved successfully.
526
	 * 
527
	 * @param array $instances
528
	 * @return int
529
	 */
530
	public static function saveMany($instances)
531
	{
532
		$failed = 0;
533
		
534
		foreach ($instances as $instance) {
535
			if (!$instance->save()) {
536
				$failed++;
537
			}
538
		}
539
		
540
		return count($instances) - $failed;
541
	}
542
	
543
	/**
544
	 * Delete the record from storage.
545
	 * 
546
	 * @return bool
547
	 */
548
	public function delete()
549
	{
550
		if ($this->id()) {
551
			$storage = $this->storage();
552
			
553
			if ($storage instanceof Modifiable) {
554
				return (bool) $storage->delete($this->table(), array($this->key() => $this->id()), 1);
555
			}
556
		}
557
		
558
		return false;
559
	}
560
	
561
	/**
562
	 * Retrieve the list of relation attributes for this model.
563
	 * 
564
	 * @return array
565
	 */
566
	public function relationAttributes()
567
	{
568
		return array_keys($this->relations);
569
	}
570
	
571
	/**
572
	 * Determine whether the given attribute is a relation.
573
	 * 
574
	 * @param string $attribute
575
	 * @return bool
576
	 */
577
	protected function hasRelation($attribute)
578
	{
579
		$attribute = $this->prepareAttribute($attribute);
580
		
581
		return isset($this->relations[$attribute]);
582
	}
583
	
584
	/**
585
	 * Retrieve the given relation.
586
	 * 
587
	 * @param string $attribute
588
	 * @return Relation
589
	 */
590
	public function relation($attribute)
591
	{
592
		if (!$this->hasRelation($attribute)) {
593
			return null;
594
		}
595
		
596
		$attribute = $this->prepareAttribute($attribute);
597
		$relation = $this->relations[$attribute];
598
		
599
		if (!$relation instanceof Relation) {
600
			$type = array_shift($relation);
601
			$arguments = array_merge(array($this), $relation);
602
			$arguments['name'] = $attribute;
603
			
604
			$relation = Relation::factory($type, $arguments);
605
			
606
			$this->relations[$attribute] = $relation;
607
		}
608
		
609
		return $relation;
610
	}
611
	
612
	/**
613
	 * Retrieve all relations.
614
	 * 
615
	 * @return Relation[]
616
	 */
617
	public function relations()
618
	{
619
		$relations = array();
620
		
621
		foreach ($this->relationAttributes() as $attribute) {
622
			$relations[$attribute] = $this->relation($attribute);
623
		}
624
		
625
		return $relations;
626
	}
627
	
628
	/**
629
	 * Determine whether the given relation has any set model(s).
630
	 * 
631
	 * @param string $attribute
632
	 * @return bool
633
	 */
634
	protected function hasRelated($attribute)
635
	{
636
		$attribute = $this->prepareAttribute($attribute);
637
		
638
		return $this->hasRelation($attribute) && $this->relation($attribute)->count();
639
	}
640
	
641
	/**
642
	 * Retrieve the model(s) of the given relation.
643
	 * 
644
	 * @param string $attribute
645
	 * @return array
646
	 */
647
	public function related($attribute)
648
	{
649
		if (!$this->hasRelation($attribute)) {
650
			return null;
651
		}
652
		
653
		$attribute = $this->prepareAttribute($attribute);
654
		
655
		$relation = $this->relation($attribute);
656
		
657
		return $relation->retrieve();
658
	}
659
	
660
	/**
661
	 * Set the given related model(s).
662
	 * 
663
	 * @param string $attribute
664
	 * @param mixed  $value
665
	 */
666
	protected function setRelated($attribute, $value)
667
	{
668
		if (!$this->hasRelation($attribute)) {
669
			return;
670
		}
671
		
672
		$relation = $this->relation($attribute);
673
		
674
		if ($value !== null && !$value instanceof $relation->target && !is_array($value)) {
675
			return;
676
		}
677
		
678
		$relation->associate($value);
679
	}
680
681
	/**
682
	 * Retrieve the default search attributes for the model.
683
	 * 
684
	 * @return array
685
	 */
686
	public function defaultSearchAttributes()
687
	{
688
		return $this->search;
689
	}
690
	
691
	/**
692
	 * Retrieve a relation. Shortcut for `relation()`.
693
	 * 
694
	 * @param string $method
695
	 * @param array  $arguments
696
	 * @return Relation
697
	 */
698
	public function __call($method, $arguments)
699
	{
700
		return $this->relation($method);
701
	}
702
}
703