Completed
Push — master ( d9c668...dafc34 )
by Chris
02:53
created

Record::hasRelation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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
	 * @var Readable Instance storage
32
	 */
33
	protected $storage;
34
	
35
	/**
36
	 * @var Readable Shared storage
37
	 */
38
	protected static $sharedStorage;
39
	
40
	/**
41
	 * @var array Definitions of related models
42
	 */
43
	protected $relations = array();
44
	
45
	/**
46
	 * @var array Default searchable attributes
47
	 */
48
	protected $search = array();
49
	
50
	/**
51
	 * Instantiate a new record with the given data or load an instance from
52
	 * storage if the given data is a valid primary key.
53
	 * 
54
	 * @param mixed $data An array of key-value attributes to set or a primary key to load by
55
	 */
56
	public function __construct($data = null) {
57
		if (is_numeric($data) || is_string($data)) {
58
			$this->data = static::load($data);
59
		}
60
		
61
		parent::__construct($data);
62
	}
63
	
64
	/**
65
	 * Determine whether the given attribute or relation is set on the record.
66
	 * 
67
	 * @param string $attribute
68
	 */
69
	public function has($attribute) {
70
		return $this->hasRelated($attribute) || parent::has($attribute);
71
	}
72
	
73
	/**
74
	 * Retrieve the given attribute or relation from the record.
75
	 * 
76
	 * @param string $attribute
77
	 * @return mixed
78
	 */
79
	public function get($attribute) {
80
		if ($this->hasRelation($attribute)) {
81
			return $this->related($attribute);
82
		}
83
		
84
		return parent::get($attribute);
85
	}
86
	
87
	/**
88
	 * Set the value of an attribute or relation on the model.
89
	 * 
90
	 * @param string $attribute
91
	 * @param mixed  $value
92
	 */
93
	public function set($attribute, $value = null) {
94
		if (is_string($attribute) && $this->hasRelation($attribute)) {
95
			return $this->setRelated($attribute, $value);
96
		}
97
		
98
		parent::set($attribute, $value);
99
	}
100
	
101
	/**
102
	 * Retrieve the name of the table this model belongs to.
103
	 * 
104
	 * If none is set, it defaults to creating it from the class name.
105
	 * 
106
	 * For example:
107
	 *     Page        -> pages
108
	 *     PageSection -> page_sections
109
	 * 
110
	 * @return string
111
	 */
112
	public function table() {
113
		if ($this->table) {
114
			return $this->table;
115
		}
116
		
117
		return preg_replace_callback('/([A-Z])/', function($matches) {
118
			return '_' . strtolower($matches[1]);
119
		}, lcfirst(basename(get_class($this)))) . 's';
120
	}
121
	
122
	/**
123
	 * Get and optionally set the model's storage instance.
124
	 * 
125
	 * @return Readable
126
	 */
127
	public function storage(Readable $storage = null) {
128
		$this->storage = $storage ?: $this->storage;
129
		
130
		return $this->storage ?: static::getSharedStorage();
131
	}
132
	
133
	/**
134
	 * Get the storage shared to all instances of this model.
135
	 * 
136
	 * @return Readable
137
	 */
138
	public static function getSharedStorage() {
139
		return static::$sharedStorage;
140
	}
141
	
142
	/**
143
	 * Share the given database connection to all instances of this model.
144
	 * 
145
	 * @param Readable $storage
146
	 */
147
	public static function setSharedStorage(Readable $storage) {
148
		static::$sharedStorage = $storage;
149
	}
150
	
151
	/**
152
	 * Prepare the record's data for storage. This is here until repositories
153
	 * are implemented.
154
	 * 
155
	 * @return array
156
	 */
157
	protected function prepareData() {
158
		$types = $this->attributes;
159
		
160
		$changed = array_intersect_key($this->data, array_flip($this->changed));
161
		
162
		$data = $this->id() && $changed ? $changed : $this->data;
163
		
164
		foreach ($data as $attribute => $value) {
165
			if (isset($types[$attribute])) {
166
				$type = $types[$attribute];
167
				
168
				switch ($type) {
169
					case 'int':
170
						$value = (int) $value;
171
						break;
172
					case 'date':
173
						$value = date('Y-m-d', $value);
174
						break;
175
					case 'datetime':
176
						$value = date('Y-m-d H:i:s', $value);
177
						break;
178
					case 'time':
179
						$value = date('H:i:s', $value);
180
						break;
181
				}
182
				
183
				$data[$attribute] = $value;
184
			}
185
		}
186
		
187
		return $data;
188
	}
189
	
190
	/**
191
	 * Prepare the given filter.
192
	 * 
193
	 * Creates a filter for the record's key attribute if the given value is not
194
	 * an array.
195
	 * 
196
	 * @param mixed $filter
197
	 * @return string
198
	 */
199
	protected static function prepareFilter($filter) {
200
		if ($filter === null) {
201
			return array();
202
		}
203
		
204
		if (!is_array($filter)) {
205
			$instance = new static;
206
			$filter = array($instance->key() => $filter);
207
		}
208
		
209
		return $filter;
210
	}
211
	
212
	/**
213
	 * Prepare the given list data.
214
	 * 
215
	 * @param array  $data
216
	 * @param string $attribute
217
	 * @return array
218
	 */
219
	protected static function prepareListing($data, $attribute) {
220
		$instance = new static;
221
		$key = $instance->key();
222
		
223
		$list = array();
224
		
225
		foreach ($data as $row) {
226
			if (isset($row[$attribute])) {
227
				$list[$row[$key]] = $row[$attribute];
228
			}
229
		}
230
		
231
		return $list;
232
	}
233
	
234
	/**
235
	 * Load record data from storage using the given criteria.
236
	 * 
237
	 * @param array|string|int $filter [optional]
238
	 * @param array|string     $order  [optional]
239
	 * @param int              $limit  [optional]
240
	 * @param int              $offset [optional]
241
	 * @return array
242
	 */
243
	public static function load($filter = array(), $order = array(), $limit = null, $offset = 0) {
244
		$instance = new static;
245
		$storage = $instance->storage();
246
		$filter = static::prepareFilter($filter);
247
		
248
		return $storage->read($instance->table(), $filter, $order, $limit, $offset);
249
	}
250
	
251
	/**
252
	 * Load a record instance from storage using the given criteria.
253
	 * 
254
	 * Returns false if the record cannot be found.
255
	 * 
256
	 * @param array|string|int $filter [optional]
257
	 * @param array|string     $order  [optional]
258
	 * @return Record|bool
259
	 */
260
	public static function find($filter = array(), $order = array()) {
261
		$data = static::load($filter, $order, 1);
262
		
263
		return !empty($data[0]) ? new static($data[0]) : false;
264
	}
265
	
266
	/**
267
	 * Load a record instance from storage using the given criteria or create a
268
	 * new instance if nothing is found.
269
	 * 
270
	 * @param array|string|int $filter [optional]
271
	 * @param array|string     $order  [optional]
272
	 * @return Record
273
	 */
274
	public static function findOrNew($filter = array(), $order = array()) {
275
		$instance = static::find($filter, $order);
276
		
277
		return $instance === false ? new static : $instance;
278
	}
279
280
	/**
281
	 * Load multiple record instances from storage using the given criteria.
282
	 * 
283
	 * @param array|string|int $filter [optional]
284
	 * @param array|string     $order  [optional]
285
	 * @param int              $limit  [optional]
286
	 * @param int              $offset [optional]
287
	 * @return array
288
	 */
289
	public static function all($filter = array(), $order = array(), $limit = null, $offset = 0) {
290
		return static::hydrate(static::load($filter, $order, $limit, $offset));
291
	}
292
	
293
	/**
294
	 * Eagerly load the given relations of multiple record instances.
295
	 * 
296
	 * @param array|string $relations
297
	 * @return array
298
	 */
299
	public static function eager($relations) {
300
		$instance = new static;
301
		$instances = static::all();
302
		
303
		foreach ((array) $relations as $relation) {
304
			if ($instance->relation($relation)) {
305
				$instances = $instance->relation($relation)->eager($instances, $relation);
306
			}
307
		}
308
		
309
		return $instances;
310
	}
311
	
312
	/**
313
	 * Search for record instances in storage using the given criteria.
314
	 * 
315
	 * @param string           $query
316
	 * @param array            $attributes [optional]
317
	 * @param array|string|int $filter     [optional]
318
	 * @param array|string     $order      [optional]
319
	 * @param int              $limit      [optional]
320
	 * @param int              $offset     [optional]
321
	 * @return array
322
	 */
323
	public static function search($query, $attributes = array(), $filter = array(), $order = array(), $limit = null, $offset = 0) {
324
		$instance = new static;
325
		$storage = $instance->storage();
326
		
327
		if (!$storage instanceof Searchable) {
328
			throw new Exception(get_class($instance) . ' storage is not searchable');
329
		}
330
		
331
		$attributes = $attributes ?: $instance->defaultSearchAttributes();
332
		
333
		$data = $storage->search($instance->table(), $query, $attributes, $filter, $order, $limit, $offset);
334
		
335
		return static::hydrate($data);
336
	}
337
	
338
	/**
339
	 * Retrieve key => value pairs using `id` for keys and the given attribute
340
	 * for values.
341
	 * 
342
	 * @param string $attribute
343
	 * @param array  $filter    [optional]
344
	 * @param array  $order     [optional]
345
	 * @param int    $limit     [optional]
346
	 * @param int    $offset    [optional]
347
	 * @return array
348
	 */
349
	public static function listing($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0) {
350
		$instance = new static;
351
		$storage = $instance->storage();
352
		
353
		$data = $storage->listing($instance->table(), array($instance->key(), $attribute), $filter, $order, $limit, $offset);
354
		
355
		return static::prepareListing($data, $attribute);
356
	}
357
	
358
	/**
359
	 * Retrieve the distinct values of the given attribute.
360
	 * 
361
	 * @param string $attribute
362
	 * @param array  $filter    [optional]
363
	 * @param array  $order     [optional]
364
	 * @param int    $limit     [optional]
365
	 * @param int    $offset    [optional]
366
	 * @return array
367
	 */
368
	public static function distinct($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0) {
369
		$instance = new static;
370
		$storage = $instance->storage();
371
		
372
		if (!$storage instanceof Aggregational) {
373
			return array_values(array_unique(static::listing($attribute, $filter, $order)));
374
		}
375
		
376
		return $storage->distinct($instance->table(), $attribute, $filter, $order, $limit, $offset);
377
	}
378
	
379
	/**
380
	 * Create a query builder for the model.
381
	 * 
382
	 * @return Builder
383
	 */
384
	public static function query() {
385
		$instance = new static;
386
		$storage = $instance->storage();
387
		
388
		if (!$storage instanceof Queryable) {
389
			throw new Exception(get_class($instance) . ' storage is not queryable');
390
		}
391
		
392
		$query = new Query($instance->table());
393
		$builder = new Builder($query, $instance->storage());
0 ignored issues
show
Documentation introduced by
$instance->storage() is of type object<Darya\Storage\Readable>, but the function expects a object<Darya\Storage\Queryable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
394
		
395
		$builder->callback(function($result) use ($instance) {
396
			return $instance::hydrate($result->data);
397
		});
398
		
399
		return $builder;
400
	}
401
	
402
	/**
403
	 * Save the record to storage.
404
	 * 
405
	 * @return bool
406
	 */
407
	public function save() {
408
		if ($this->validate()) {
409
			$storage = $this->storage();
410
			$class = get_class($this);
411
			
412
			if (!$storage instanceof Modifiable) {
413
				throw new Exception(basename($class) . ' storage is not modifiable');
414
			}
415
			
416
			$data = $this->prepareData();
417
			
418
			if (!$this->id()) {
419
				$id = $storage->create($this->table(), $data);
420
				
421
				if ($id) {
422
					$this->set($this->key(), $id);
423
					$this->reinstate();
424
					
425
					return true;
426
				}
427
			} else {
428
				$updated = $storage->update($this->table(), $data, array($this->key() => $this->id()), 1);
429
				
430
				if (!$updated) {
431
					$updated = $storage->create($this->table(), $data) > 0;
432
				}
433
				
434
				if ($updated) {
435
					$this->reinstate();
436
					
437
					return true;
438
				}
439
			}
440
			
441
			$entity = strtolower(basename($class));
442
			$this->errors['save'] = "Failed to save $entity";
443
			$this->errors['storage'] = $this->storage()->error();
444
		}
445
		
446
		return false;
447
	}
448
	
449
	/**
450
	 * Save multiple record instances to storage.
451
	 * 
452
	 * Returns the number of instances that saved successfully.
453
	 * 
454
	 * @param array $instances
455
	 * @return int
456
	 */
457
	public static function saveMany($instances) {
458
		$failed = 0;
459
		
460
		foreach ($instances as $instance) {
461
			if (!$instance->save()) {
462
				$failed++;
463
			}
464
		}
465
		
466
		return count($instances) - $failed;
467
	}
468
	
469
	/**
470
	 * Delete the record from storage.
471
	 * 
472
	 * @return bool
473
	 */
474
	public function delete() {
475
		if ($this->id()) {
476
			$storage = $this->storage();
477
			
478
			if ($storage instanceof Modifiable) {
479
				return (bool) $storage->delete($this->table(), array($this->key() => $this->id()), 1);
480
			}
481
		}
482
		
483
		return false;
484
	}
485
	
486
	/**
487
	 * Retrieve the list of relation attributes for this model.
488
	 * 
489
	 * @return array
490
	 */
491
	public function relationAttributes() {
492
		return array_keys($this->relations);
493
	}
494
	
495
	/**
496
	 * Determine whether the given attribute is a relation.
497
	 * 
498
	 * @param string $attribute
499
	 * @return bool
500
	 */
501
	protected function hasRelation($attribute) {
502
		$attribute = $this->prepareAttribute($attribute);
503
		
504
		return isset($this->relations[$attribute]);
505
	}
506
	
507
	/**
508
	 * Retrieve the given relation.
509
	 * 
510
	 * @param string $attribute
511
	 * @return Relation
512
	 */
513
	public function relation($attribute) {
514
		if (!$this->hasRelation($attribute)) {
515
			return null;
516
		}
517
		
518
		$attribute = $this->prepareAttribute($attribute);
519
		$relation = $this->relations[$attribute];
520
		
521
		if (!$relation instanceof Relation) {
522
			$type = array_shift($relation);
523
			$arguments = array_merge(array($this), $relation);
524
			
525
			$relation = Relation::factory($type, $arguments);
526
			
527
			$this->relations[$attribute] = $relation;
528
		}
529
		
530
		return $relation;
531
	}
532
	
533
	/**
534
	 * Retrieve all relations.
535
	 * 
536
	 * @return Relation[]
537
	 */
538
	public function relations() {
539
		$relations = array();
540
		
541
		foreach ($this->relationAttributes() as $attribute) {
542
			$relations = $this->relation($attribute);
543
		}
544
		
545
		return $relations;
546
	}
547
	
548
	/**
549
	 * Determine whether the given relation has any set model(s).
550
	 * 
551
	 * @param string $attribute
552
	 * @return bool
553
	 */
554
	protected function hasRelated($attribute) {
555
		$attribute = $this->prepareAttribute($attribute);
556
		
557
		return $this->hasRelation($attribute) && $this->relation($attribute)->count();
558
	}
559
	
560
	/**
561
	 * Retrieve the model(s) of the given relation.
562
	 * 
563
	 * @param string $attribute
564
	 * @return array
565
	 */
566
	public function related($attribute) {
567
		if (!$this->hasRelation($attribute)) {
568
			return null;
569
		}
570
		
571
		$attribute = $this->prepareAttribute($attribute);
572
		
573
		$relation = $this->relation($attribute);
574
		
575
		return $relation->retrieve();
576
	}
577
	
578
	/**
579
	 * Set the given related model(s).
580
	 * 
581
	 * @param string $attribute
582
	 * @param mixed  $value
583
	 */
584
	protected function setRelated($attribute, $value) {
585
		if (!$this->hasRelation($attribute)) {
586
			return;
587
		}
588
		
589
		$relation = $this->relation($attribute);
590
		
591
		if ($value !== null && !$value instanceof $relation->target && !is_array($value)) {
592
			return;
593
		}
594
		
595
		$relation->associate($value);
596
	}
597
598
	/**
599
	 * Retrieve the default search attributes for the model.
600
	 * 
601
	 * @return array
602
	 */
603
	public function defaultSearchAttributes() {
604
		return $this->search;
605
	}
606
	
607
	/**
608
	 * Retrieve a relation. Shortcut for `relation()`.
609
	 * 
610
	 * @param string $method
611
	 * @param array  $arguments
612
	 * @return Relation
613
	 */
614
	public function __call($method, $arguments) {
615
		return $this->relation($method);
616
	}
617
	
618
}
619