Completed
Push — master ( 2c1d81...7608f4 )
by Chris
05:45
created

Record::prepareListing()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 14
rs 9.4286
c 2
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
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 \Darya\Storage\Readable Instance storage
32
	 */
33
	protected $storage;
34
	
35
	/**
36
	 * @var \Darya\Storage\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
	 * Generate instances of the model with the given sets of attributes.
66
	 * 
67
	 * @param array $rows
68
	 * @return Record[]
69
	 */
70 View Code Duplication
	public static function generate(array $rows = array()) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
71
		$instances = array();
72
		
73
		foreach ($rows as $key => $attributes) {
74
			$instances[$key] = new static;
75
			$instances[$key]->setMany($attributes);
76
		}
77
		
78
		return $instances;
79
	}
80
	
81
	/**
82
	 * Determine whether the given attribute or relation is set on the record.
83
	 * 
84
	 * @param string $attribute
85
	 */
86
	public function has($attribute) {
87
		return $this->hasRelated($attribute) || parent::has($attribute);
88
	}
89
	
90
	/**
91
	 * Retrieve the given attribute or relation from the record.
92
	 * 
93
	 * @param string $attribute
94
	 * @return mixed
95
	 */
96
	public function get($attribute) {
97
		if ($this->hasRelation($attribute)) {
98
			return $this->related($attribute);
99
		}
100
		
101
		return parent::get($attribute);
102
	}
103
	
104
	/**
105
	 * Set the value of an attribute or relation on the model.
106
	 * 
107
	 * @param string $attribute
108
	 * @param mixed  $value
109
	 */
110
	public function set($attribute, $value = null) {
111
		if (is_string($attribute) && $this->hasRelation($attribute)) {
112
			return $this->setRelated($attribute, $value);
113
		}
114
		
115
		parent::set($attribute, $value);
116
	}
117
	
118
	/**
119
	 * Retrieve the name of the table this model belongs to.
120
	 * 
121
	 * If none is set, it defaults to creating it from the class name.
122
	 * 
123
	 * For example:
124
	 *     Page        -> pages
125
	 *     PageSection -> page_sections
126
	 * 
127
	 * @return string
128
	 */
129
	public function table() {
130
		if ($this->table) {
131
			return $this->table;
132
		}
133
		
134
		return preg_replace_callback('/([A-Z])/', function($matches) {
135
			return '_' . strtolower($matches[1]);
136
		}, lcfirst(basename(get_class($this)))) . 's';
137
	}
138
	
139
	/**
140
	 * Get and optionally set the model's storage instance.
141
	 * 
142
	 * @return \Darya\Storage\Readable
143
	 */
144
	public function storage(Readable $storage = null) {
145
		$this->storage = $storage ?: $this->storage;
146
		
147
		return $this->storage ?: static::getSharedStorage();
148
	}
149
	
150
	/**
151
	 * Get the storage shared to all instances of this model.
152
	 * 
153
	 * @return \Darya\Storage\Readable
154
	 */
155
	public static function getSharedStorage() {
156
		return static::$sharedStorage;
157
	}
158
	
159
	/**
160
	 * Share the given database connection to all instances of this model.
161
	 * 
162
	 * @param \Darya\Storage\Readable $storage
163
	 */
164
	public static function setSharedStorage(Readable $storage) {
165
		static::$sharedStorage = $storage;
166
	}
167
	
168
	/**
169
	 * Prepare the record's data for storage. This is here until repositories
170
	 * are implemented.
171
	 * 
172
	 * @return array
173
	 */
174
	protected function prepareData() {
175
		$types = $this->attributes;
176
		
177
		$changed = array_intersect_key($this->data, array_flip($this->changed));
178
		
179
		$data = $this->id() && $changed ? $changed : $this->data;
180
		
181
		foreach ($data as $attribute => $value) {
182
			if (isset($types[$attribute])) {
183
				$type = $types[$attribute];
184
				
185
				switch ($type) {
186
					case 'int':
187
						$value = (int) $value;
188
						break;
189
					case 'date':
190
						$value = date('Y-m-d', $value);
191
						break;
192
					case 'datetime':
193
						$value = date('Y-m-d H:i:s', $value);
194
						break;
195
					case 'time':
196
						$value = date('H:i:s', $value);
197
						break;
198
				}
199
				
200
				$data[$attribute] = $value;
201
			}
202
		}
203
		
204
		return $data;
205
	}
206
	
207
	/**
208
	 * Prepare the given filter.
209
	 * 
210
	 * Creates a filter for the record's key attribute if the given value is not
211
	 * an array.
212
	 * 
213
	 * @param mixed $filter
214
	 * @return string
215
	 */
216
	protected static function prepareFilter($filter) {
217
		if ($filter === null) {
218
			return array();
219
		}
220
		
221
		if (!is_array($filter)) {
222
			$instance = new static;
223
			$filter = array($instance->key() => $filter);
224
		}
225
		
226
		return $filter;
227
	}
228
	
229
	/**
230
	 * Prepare the given list data.
231
	 * 
232
	 * @param array  $data
233
	 * @param string $attribute
234
	 * @return array
235
	 */
236
	protected static function prepareListing($data, $attribute) {
237
		$instance = new static;
238
		$key = $instance->key();
239
		
240
		$list = array();
241
		
242
		foreach ($data as $row) {
243
			if (isset($row[$attribute])) {
244
				$list[$row[$key]] = $row[$attribute];
245
			}
246
		}
247
		
248
		return $list;
249
	}
250
	
251
	/**
252
	 * Load record data from storage using the given criteria.
253
	 * 
254
	 * @param array|string|int $filter [optional]
255
	 * @param array|string     $order  [optional]
256
	 * @param int              $limit  [optional]
257
	 * @param int              $offset [optional]
258
	 * @return array
259
	 */
260
	public static function load($filter = array(), $order = array(), $limit = null, $offset = 0) {
261
		$instance = new static;
262
		$storage = $instance->storage();
263
		$filter = static::prepareFilter($filter);
264
		
265
		return $storage->read($instance->table(), $filter, $order, $limit, $offset);
266
	}
267
	
268
	/**
269
	 * Load a record instance from storage using the given criteria.
270
	 * 
271
	 * Returns false if the record cannot be found.
272
	 * 
273
	 * @param array|string|int $filter [optional]
274
	 * @param array|string     $order  [optional]
275
	 * @return Record|bool
276
	 */
277
	public static function find($filter = array(), $order = array()) {
278
		$data = static::load($filter, $order, 1);
279
		
280
		return !empty($data[0]) ? new static($data[0]) : false;
281
	}
282
	
283
	/**
284
	 * Load a record instance from storage using the given criteria or create a
285
	 * new instance if nothing is found.
286
	 * 
287
	 * @param array|string|int $filter [optional]
288
	 * @param array|string     $order  [optional]
289
	 * @return Record
290
	 */
291
	public static function findOrNew($filter = array(), $order = array()) {
292
		$instance = static::find($filter, $order);
293
		
294
		return $instance === false ? new static : $instance;
295
	}
296
297
	/**
298
	 * Load multiple record instances from storage using the given criteria.
299
	 * 
300
	 * @param array|string|int $filter [optional]
301
	 * @param array|string     $order  [optional]
302
	 * @param int              $limit  [optional]
303
	 * @param int              $offset [optional]
304
	 * @return array
305
	 */
306
	public static function all($filter = array(), $order = array(), $limit = null, $offset = 0) {
307
		return static::generate(static::load($filter, $order, $limit, $offset));
308
	}
309
	
310
	/**
311
	 * Eagerly load the given relations of multiple record instances.
312
	 * 
313
	 * @param array|string $relations
314
	 * @return array
315
	 */
316
	public static function eager($relations) {
317
		$instance = new static;
318
		$instances = static::all();
319
		
320
		foreach ((array) $relations as $relation) {
321
			if ($instance->relation($relation)) {
322
				$instances = $instance->relation($relation)->eager($instances, $relation);
323
			}
324
		}
325
		
326
		return $instances;
327
	}
328
	
329
	/**
330
	 * Search for record instances in storage using the given criteria.
331
	 * 
332
	 * @param string           $query
333
	 * @param array            $attributes [optional]
334
	 * @param array|string|int $filter     [optional]
335
	 * @param array|string     $order      [optional]
336
	 * @param int              $limit      [optional]
337
	 * @param int              $offset     [optional]
338
	 * @return array
339
	 */
340
	public static function search($query, $attributes = array(), $filter = array(), $order = array(), $limit = null, $offset = 0) {
341
		$instance = new static;
342
		$storage = $instance->storage();
343
		
344
		if (!$storage instanceof Searchable) {
345
			throw new Exception(get_class($instance) . ' storage is not searchable');
346
		}
347
		
348
		$attributes = $attributes ?: $instance->defaultSearchAttributes();
349
		
350
		$data = $storage->search($instance->table(), $query, $attributes, $filter, $order, $limit, $offset);
351
		
352
		return static::generate($data);
353
	}
354
	
355
	/**
356
	 * Retrieve key => value pairs using `id` for keys and the given attribute
357
	 * for values.
358
	 * 
359
	 * @param string $attribute
360
	 * @param array  $filter    [optional]
361
	 * @param array  $order     [optional]
362
	 * @param int    $limit     [optional]
363
	 * @param int    $offset    [optional]
364
	 * @return array
365
	 */
366
	public static function listing($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0) {
367
		$instance = new static;
368
		$storage = $instance->storage();
369
		
370
		$data = $storage->listing($instance->table(), array($instance->key(), $attribute), $filter, $order, $limit, $offset);
371
		
372
		return static::prepareListing($data, $attribute);
373
	}
374
	
375
	/**
376
	 * Retrieve the distinct values of the given attribute.
377
	 * 
378
	 * @param string $attribute
379
	 * @param array  $filter    [optional]
380
	 * @param array  $order     [optional]
381
	 * @param int    $limit     [optional]
382
	 * @param int    $offset    [optional]
383
	 * @return array
384
	 */
385
	public static function distinct($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0) {
386
		$instance = new static;
387
		$storage = $instance->storage();
388
		
389
		if (!$storage instanceof Aggregational) {
390
			return array_values(array_unique(static::listing($attribute, $filter, $order)));
391
		}
392
		
393
		return $storage->distinct($instance->table(), $attribute, $filter, $order, $limit, $offset);
394
	}
395
	
396
	/**
397
	 * Create a query builder for the model.
398
	 * 
399
	 * @return Builder
400
	 */
401
	public static function query() {
402
		$instance = new static;
403
		$storage = $instance->storage();
404
		
405
		if (!$storage instanceof Queryable) {
406
			throw new Exception(get_class($instance) . ' storage is not queryable');
407
		}
408
		
409
		$query = new Query($instance->table());
410
		$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...
411
		
412
		$builder->callback(function($result) use ($instance) {
413
			return $instance::generate($result->data);
414
		});
415
		
416
		return $builder;
417
	}
418
	
419
	/**
420
	 * Save the record to storage.
421
	 * 
422
	 * @return bool
423
	 */
424
	public function save() {
425
		if ($this->validate()) {
426
			$storage = $this->storage();
427
			$class = get_class($this);
428
			
429
			if (!$storage instanceof Modifiable) {
430
				throw new Exception(basename($class) . ' storage is not modifiable');
431
			}
432
			
433
			$data = $this->prepareData();
434
			
435
			if (!$this->id()) {
436
				$id = $storage->create($this->table(), $data);
437
				
438
				if ($id) {
439
					$this->set($this->key(), $id);
440
					
441
					return true;
442
				}
443
			} else {
444
				$updated = $storage->update($this->table(), $data, array($this->key() => $this->id()), 1);
445
				
446
				if (!$updated) {
447
					$updated = $storage->create($this->table(), $data) > 0;
448
				}
449
				
450
				if ($updated) {
451
					return true;
452
				}
453
			}
454
			
455
			$entity = strtolower(basename($class));
456
			$this->errors['save'] = "Failed to save $entity";
457
			$this->errors['storage'] = $this->storage()->error();
458
		}
459
		
460
		return false;
461
	}
462
	
463
	/**
464
	 * Save multiple record instances to storage.
465
	 * 
466
	 * Returns the number of instances that saved successfully.
467
	 * 
468
	 * @param array $instances
469
	 * @return int
470
	 */
471
	public static function saveMany($instances) {
472
		$failed = 0;
473
		
474
		foreach ($instances as $instance) {
475
			if (!$instance->save()) {
476
				$failed++;
477
			}
478
		}
479
		
480
		return count($instances) - $failed;
481
	}
482
	
483
	/**
484
	 * Delete the record from storage.
485
	 * 
486
	 * @return bool
487
	 */
488
	public function delete() {
489
		if ($this->id()) {
490
			$storage = $this->storage();
491
			
492
			if ($storage instanceof Modifiable) {
493
				return (bool) $storage->delete($this->table(), array($this->key() => $this->id()), 1);
494
			}
495
		}
496
		
497
		return false;
498
	}
499
	
500
	/**
501
	 * Determine whether the given attribute is a relation.
502
	 * 
503
	 * @param string $attribute
504
	 * @return bool
505
	 */
506
	protected function hasRelation($attribute) {
507
		$attribute = $this->prepareAttribute($attribute);
508
		
509
		return isset($this->relations[$attribute]);
510
	}
511
	
512
	/**
513
	 * Retrieve the given relation.
514
	 * 
515
	 * @param string $attribute
516
	 * @return \Darya\ORM\Relation
517
	 */
518
	public function relation($attribute) {
519
		if (!$this->hasRelation($attribute)) {
520
			return null;
521
		}
522
		
523
		$attribute = $this->prepareAttribute($attribute);
524
		$relation = $this->relations[$attribute];
525
		
526
		if (!$relation instanceof Relation) {
527
			$type = array_shift($relation);
528
			$arguments = array_merge(array($this), $relation);
529
			
530
			$relation = Relation::factory($type, $arguments);
531
			
532
			$this->relations[$attribute] = $relation;
533
		}
534
		
535
		return $relation;
536
	}
537
	
538
	/**
539
	 * Determine whether the given relation has any set model(s).
540
	 * 
541
	 * @param string $attribute
542
	 * @return bool
543
	 */
544
	protected function hasRelated($attribute) {
545
		$attribute = $this->prepareAttribute($attribute);
546
		
547
		return $this->hasRelation($attribute) && $this->relation($attribute)->count();
548
	}
549
	
550
	/**
551
	 * Retrieve the model(s) of the given relation.
552
	 * 
553
	 * @param string $attribute
554
	 * @return array
555
	 */
556
	public function related($attribute) {
557
		if (!$this->hasRelation($attribute)) {
558
			return null;
559
		}
560
		
561
		$attribute = $this->prepareAttribute($attribute);
562
		
563
		$relation = $this->relation($attribute);
564
		
565
		return $relation->retrieve();
566
	}
567
	
568
	/**
569
	 * Set the given related model(s).
570
	 * 
571
	 * @param string $attribute
572
	 * @param mixed  $value
573
	 */
574
	protected function setRelated($attribute, $value) {
575
		if (!$this->hasRelation($attribute)) {
576
			return;
577
		}
578
		
579
		$relation = $this->relation($attribute);
580
		
581
		if ($value !== null && !$value instanceof $relation->target && !is_array($value)) {
582
			return;
583
		}
584
		
585
		$relation->associate($value);
586
	}
587
588
	/**
589
	 * Retrieve the default search attributes for the model.
590
	 * 
591
	 * @return array
592
	 */
593
	public function defaultSearchAttributes() {
594
		return $this->search;
595
	}
596
	
597
	/**
598
	 * Retrieve a relation. Shortcut for `relation()`.
599
	 * 
600
	 * @param string $method
601
	 * @param array  $arguments
602
	 * @return Relation
603
	 */
604
	public function __call($method, $arguments) {
605
		return $this->relation($method);
606
	}
607
	
608
}
609