Record::prepareData()   B
last analyzed

Complexity

Conditions 8
Paths 14

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 21
c 2
b 0
f 0
nc 14
nop 0
dl 0
loc 32
rs 8.4444
1
<?php
2
namespace Darya\ORM;
3
4
use Exception;
5
use Darya\Storage\Query;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Darya\ORM\Query. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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
97
			if ($related instanceof Record && $subattribute !== null) {
0 ignored issues
show
introduced by
$related is never a sub-type of Darya\ORM\Record.
Loading history...
98
				return $related->get($subattribute);
99
			}
100
101
			return $related;
102
		}
103
104
		return parent::get($attribute);
105
	}
106
107
	/**
108
	 * Set the value of an attribute or relation on the model.
109
	 *
110
	 * @param string $attribute
111
	 * @param mixed  $value
112
	 */
113
	public function set($attribute, $value = null)
114
	{
115
		if (is_string($attribute) && $this->hasRelation($attribute)) {
116
			$this->setRelated($attribute, $value);
117
118
			return;
119
		}
120
121
		parent::set($attribute, $value);
122
	}
123
124
	/**
125
	 * Unset the value for an attribute or relation on the model.
126
	 *
127
	 * @param string $attribute
128
	 */
129
	public function remove($attribute)
130
	{
131
		if ($this->hasRelation($attribute)) {
132
			return $this->unsetRelated($attribute);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->unsetRelated($attribute) targeting Darya\ORM\Record::unsetRelated() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
133
		}
134
135
		parent::remove($attribute);
136
	}
137
138
	/**
139
	 * Retrieve the name of the table this model belongs to.
140
	 *
141
	 * If none is set, it defaults to creating it from the class name.
142
	 *
143
	 * For example:
144
	 *     Page        -> pages
145
	 *     PageSection -> page_sections
146
	 *
147
	 * @return string
148
	 */
149
	public function table()
150
	{
151
		if ($this->table) {
152
			return $this->table;
153
		}
154
155
		$split = explode('\\', get_class($this));
156
		$class = end($split);
157
158
		return preg_replace_callback('/([A-Z])/', function ($matches) {
159
			return '_' . strtolower($matches[1]);
160
		}, lcfirst($class)) . 's';
161
	}
162
163
	/**
164
	 * Get and optionally set the model's storage instance.
165
	 *
166
	 * @param Readable $storage [optional]
167
	 * @return Readable
168
	 */
169
	public function storage(Readable $storage = null)
170
	{
171
		$this->storage = $storage ?: $this->storage;
172
173
		return $this->storage ?: static::getSharedStorage();
174
	}
175
176
	/**
177
	 * Get the storage shared to all instances of this model.
178
	 *
179
	 * @return Readable
180
	 */
181
	public static function getSharedStorage()
182
	{
183
		return static::$sharedStorage;
184
	}
185
186
	/**
187
	 * Share the given database connection to all instances of this model.
188
	 *
189
	 * @param Readable $storage
190
	 */
191
	public static function setSharedStorage(Readable $storage)
192
	{
193
		static::$sharedStorage = $storage;
194
	}
195
196
	/**
197
	 * Prepare the record's data for storage.
198
	 *
199
	 * This is here until repositories are implemented.
200
	 *
201
	 * @return array
202
	 */
203
	protected function prepareData()
204
	{
205
		$types = $this->attributes;
206
207
		$changed = array_intersect_key($this->data, array_flip($this->changed));
208
209
		$data = $this->id() ? $changed : $this->data;
210
211
		foreach ($data as $attribute => $value) {
212
			if (isset($types[$attribute])) {
213
				$type = $types[$attribute];
214
215
				switch ($type) {
216
					case 'int':
217
						$value = (int) $value;
218
						break;
219
					case 'date':
220
						$value = date('Y-m-d', $value);
221
						break;
222
					case 'datetime':
223
						$value = date('Y-m-d H:i:s', $value);
224
						break;
225
					case 'time':
226
						$value = date('H:i:s', $value);
227
						break;
228
				}
229
230
				$data[$attribute] = $value;
231
			}
232
		}
233
234
		return $data;
235
	}
236
237
	/**
238
	 * Prepare the given filter.
239
	 *
240
	 * Creates a filter for the record's key attribute if the given value is not
241
	 * an array.
242
	 *
243
	 * TODO: Filter by key if $filter has numeric keys
244
	 *
245
	 * @param mixed $filter
246
	 * @return string
247
	 */
248
	protected static function prepareFilter($filter)
249
	{
250
		if ($filter === null) {
251
			return array();
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type string.
Loading history...
252
		}
253
254
		if (!is_array($filter)) {
255
			$instance = new static;
256
			$filter = array($instance->key() => $filter);
257
		}
258
259
		return $filter;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $filter returns the type array|array<string,mixed> which is incompatible with the documented return type string.
Loading history...
260
	}
261
262
	/**
263
	 * Prepare the given list data.
264
	 *
265
	 * @param array  $data
266
	 * @param string $attribute
267
	 * @return array
268
	 */
269
	protected static function prepareListing($data, $attribute)
270
	{
271
		$instance = new static;
272
		$key = $instance->key();
273
274
		$list = array();
275
276
		foreach ($data as $row) {
277
			if (isset($row[$attribute])) {
278
				$list[$row[$key]] = $row[$attribute];
279
			}
280
		}
281
282
		return $list;
283
	}
284
285
	/**
286
	 * Load record data from storage using the given criteria.
287
	 *
288
	 * @param array|string|int $filter [optional]
289
	 * @param array|string     $order  [optional]
290
	 * @param int              $limit  [optional]
291
	 * @param int              $offset [optional]
292
	 * @return array
293
	 */
294
	public static function load($filter = array(), $order = array(), $limit = 0, $offset = 0)
295
	{
296
		$instance = new static;
297
		$storage = $instance->storage();
298
		$filter = static::prepareFilter($filter);
299
300
		return $storage->read($instance->table(), $filter, $order, $limit, $offset);
0 ignored issues
show
Bug introduced by
$filter of type string is incompatible with the type array expected by parameter $filter of Darya\Storage\Readable::read(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

300
		return $storage->read($instance->table(), /** @scrutinizer ignore-type */ $filter, $order, $limit, $offset);
Loading history...
301
	}
302
303
	/**
304
	 * Load a record instance from storage using the given criteria.
305
	 *
306
	 * Returns false if the record cannot be found.
307
	 *
308
	 * @param array|string|int $filter [optional]
309
	 * @param array|string     $order  [optional]
310
	 * @return Record|bool
311
	 */
312
	public static function find($filter = array(), $order = array())
313
	{
314
		$data = static::load($filter, $order, 1);
315
316
		if (empty($data[0])) {
317
			return false;
318
		}
319
320
		$instance = new static($data[0]);
321
322
		$instance->reinstate();
323
324
		return $instance;
325
	}
326
327
	/**
328
	 * Load a record instance from storage using the given criteria or create a
329
	 * new instance if nothing is found.
330
	 *
331
	 * @param array|string|int $filter [optional]
332
	 * @param array|string     $order  [optional]
333
	 * @return Record
334
	 */
335
	public static function findOrNew($filter = array(), $order = array())
336
	{
337
		$instance = static::find($filter, $order);
338
339
		if ($instance === false) {
340
			return new static;
341
		}
342
343
		$instance->reinstate();
344
345
		return $instance;
346
	}
347
348
	/**
349
	 * Load multiple record instances matching the given IDs.
350
	 *
351
	 * @param array|string|int $ids
352
	 * @return array
353
	 */
354
	public static function in($ids = array())
355
	{
356
		$instance = new static;
357
358
		$key = $instance->key();
359
360
		return static::all([$key => (array) $ids]);
361
	}
362
363
	/**
364
	 * Load multiple record instances from storage using the given criteria.
365
	 *
366
	 * @param array|string|int $filter [optional]
367
	 * @param array|string     $order  [optional]
368
	 * @param int              $limit  [optional]
369
	 * @param int              $offset [optional]
370
	 * @return array
371
	 */
372
	public static function all($filter = array(), $order = array(), $limit = 0, $offset = 0)
373
	{
374
		return static::hydrate(static::load($filter, $order, $limit, $offset));
375
	}
376
377
	/**
378
	 * Eagerly load the given relations of multiple record instances.
379
	 *
380
	 * @param array|string     $relations
381
	 * @param array|string|int $filter    [optional]
382
	 * @param array|string     $order     [optional]
383
	 * @param int              $limit     [optional]
384
	 * @param int              $offset    [optional]
385
	 * @return array
386
	 */
387
	public static function eager($relations, $filter = array(), $order = array(), $limit = 0, $offset = 0)
388
	{
389
		$instance = new static;
390
		$instances = static::all($filter, $order, $limit, $offset);
391
392
		foreach ((array) $relations as $relation) {
393
			if ($instance->relation($relation)) {
394
				$instances = $instance->relation($relation)->eager($instances);
395
			}
396
		}
397
398
		return $instances;
399
	}
400
401
	/**
402
	 * Search for record instances in storage using the given criteria.
403
	 *
404
	 * @param string           $query
405
	 * @param array            $attributes [optional]
406
	 * @param array|string|int $filter     [optional]
407
	 * @param array|string     $order      [optional]
408
	 * @param int              $limit      [optional]
409
	 * @param int              $offset     [optional]
410
	 * @return array
411
	 * @throws Exception
412
	 */
413
	public static function search($query, $attributes = array(), $filter = array(), $order = array(), $limit = null, $offset = 0)
414
	{
415
		$instance = new static;
416
		$storage = $instance->storage();
417
418
		if (!$storage instanceof Searchable) {
419
			throw new Exception(get_class($instance) . ' storage is not searchable');
420
		}
421
422
		$attributes = $attributes ?: $instance->defaultSearchAttributes();
423
424
		$data = $storage->search($instance->table(), $query, $attributes, $filter, $order, $limit, $offset);
0 ignored issues
show
Bug introduced by
It seems like $filter can also be of type integer and string; however, parameter $filter of Darya\Storage\Searchable::search() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

424
		$data = $storage->search($instance->table(), $query, $attributes, /** @scrutinizer ignore-type */ $filter, $order, $limit, $offset);
Loading history...
425
426
		return static::hydrate($data);
427
	}
428
429
	/**
430
	 * Retrieve key => value pairs using `id` for keys and the given attribute
431
	 * for values.
432
	 *
433
	 * @param string $attribute
434
	 * @param array  $filter    [optional]
435
	 * @param array  $order     [optional]
436
	 * @param int    $limit     [optional]
437
	 * @param int    $offset    [optional]
438
	 * @return array
439
	 */
440
	public static function listing($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0)
441
	{
442
		$instance = new static;
443
		$storage = $instance->storage();
444
445
		$data = $storage->listing($instance->table(), array($instance->key(), $attribute), $filter, $order, $limit, $offset);
446
447
		return static::prepareListing($data, $attribute);
448
	}
449
450
	/**
451
	 * Retrieve the distinct values of the given attribute.
452
	 *
453
	 * @param string $attribute
454
	 * @param array  $filter    [optional]
455
	 * @param array  $order     [optional]
456
	 * @param int    $limit     [optional]
457
	 * @param int    $offset    [optional]
458
	 * @return array
459
	 */
460
	public static function distinct($attribute, $filter = array(), $order = array(), $limit = null, $offset = 0)
461
	{
462
		$instance = new static;
463
		$storage = $instance->storage();
464
465
		if (!$storage instanceof Aggregational) {
466
			return array_values(array_unique(static::listing($attribute, $filter, $order)));
467
		}
468
469
		return $storage->distinct($instance->table(), $attribute, $filter, $order, $limit, $offset);
470
	}
471
472
	/**
473
	 * Create a query builder for the model.
474
	 *
475
	 * @return Builder
476
	 * @throws Exception
477
	 */
478
	public static function query()
479
	{
480
		$instance = new static;
481
		$storage = $instance->storage();
482
483
		if (!$storage instanceof Queryable) {
484
			throw new Exception(get_class($instance) . ' storage is not queryable');
485
		}
486
487
		$query = new Query($instance->table());
488
		$builder = new Builder($query, $storage);
489
490
		$builder->callback(function ($result) use ($instance) {
491
			return $instance::hydrate($result->data);
492
		});
493
494
		return $builder;
495
	}
496
497
	/**
498
	 * Create the model in storage.
499
	 *
500
	 * @return bool
501
	 */
502
	protected function saveNew()
503
	{
504
		$data = $this->prepareData();
505
		$storage = $this->storage();
506
507
		// Create a new item
508
		$id = $storage->create($this->table(), $data);
0 ignored issues
show
Bug introduced by
The method create() does not exist on Darya\Storage\Readable. Since it exists in all sub-types, consider adding an abstract or default implementation to Darya\Storage\Readable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

508
		/** @scrutinizer ignore-call */ 
509
  $id = $storage->create($this->table(), $data);
Loading history...
509
510
		// Bail if saving failed
511
		if (!$id) {
512
			return false;
513
		}
514
515
		// If we didn't get a boolean back, assume this is an ID (TODO: Formalise)
516
		if (!is_bool($id)) {
517
			$this->set($this->key(), $id);
518
		}
519
520
		$this->reinstate();
521
522
		return true;
523
	}
524
525
	/**
526
	 * Update the model in storage.
527
	 *
528
	 * @return bool
529
	 */
530
	protected function saveExisting()
531
	{
532
		$data = $this->prepareData();
533
		$storage = $this->storage();
534
535
		// We can bail if there isn't any new data to save
536
		if (empty($data)) {
537
			return true;
538
		}
539
540
		// Attempt to update an existing item
541
		$updated = $storage->update($this->table(), $data, array($this->key() => $this->id()));
0 ignored issues
show
Bug introduced by
The method update() does not exist on Darya\Storage\Readable. Since it exists in all sub-types, consider adding an abstract or default implementation to Darya\Storage\Readable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

541
		/** @scrutinizer ignore-call */ 
542
  $updated = $storage->update($this->table(), $data, array($this->key() => $this->id()));
Loading history...
542
543
		// Otherwise it either doesn't exist or wasn't changed
544
		if ($updated !== 1) {
545
			// So we check whether it exists
546
			$exists = $storage->read($this->table(), array($this->key() => $this->id()));
547
548
			if ($exists) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exists of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
549
				return true;
550
			}
551
552
			// And if it doesn't, we can create it
553
			$updated = $storage->create($this->table(), $data) > 0;
554
		}
555
556
		return $updated;
557
	}
558
559
	/**
560
	 * Save the record to storage.
561
	 *
562
	 * @param array $options [optional]
563
	 * @return bool
564
	 * @throws Exception
565
	 */
566
	public function save(array $options = array())
567
	{
568
		// Bail if the model is not valid
569
		if (!$this->validate()) {
570
			return false;
571
		}
572
573
		$storage = $this->storage();
574
		$class = get_class($this);
575
576
		// Storage must be modifiable in order to save
577
		if (!$storage instanceof Modifiable) {
578
			throw new Exception($class . ' storage is not modifiable');
579
		}
580
581
		// Create or update the model in storage
582
		if (!$this->id()) {
583
			$success = $this->saveNew();
584
		} else {
585
			$success = $this->saveExisting();
586
		}
587
588
		if ($success) {
589
			// Clear the changed attributes; we're in sync now
590
			$this->reinstate();
591
592
			// Save relations if we don't want to skip
593
			if (empty($options['skipRelations'])) {
594
				$this->saveRelations();
595
			}
596
		} else {
597
			$this->errors['save'] = "Failed to save $class instance";
598
			$this->errors['storage'] = $this->storage()->error();
0 ignored issues
show
Bug introduced by
The method error() does not exist on Darya\Storage\Readable. Since it exists in all sub-types, consider adding an abstract or default implementation to Darya\Storage\Readable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

598
			$this->errors['storage'] = $this->storage()->/** @scrutinizer ignore-call */ error();
Loading history...
599
		}
600
601
		return $success;
602
	}
603
604
	/**
605
	 * Save multiple record instances to storage.
606
	 *
607
	 * Returns the number of instances that saved successfully.
608
	 *
609
	 * @param array $instances
610
	 * @param array $options   [optional]
611
	 * @return int
612
	 */
613
	public static function saveMany($instances, array $options = array())
614
	{
615
		$saved = 0;
616
617
		foreach ($instances as $instance) {
618
			if ($instance->save($options)) {
619
				$saved++;
620
			}
621
		}
622
623
		return $saved;
624
	}
625
626
	/**
627
	 * Delete the record from storage.
628
	 *
629
	 * @return bool
630
	 */
631
	public function delete()
632
	{
633
		$storage = $this->storage();
634
635
		if (!$this->id() || !($storage instanceof Modifiable)) {
636
			return false;
637
		}
638
639
		return (bool) $storage->delete($this->table(), array($this->key() => $this->id()), 1);
640
	}
641
642
	/**
643
	 * Retrieve the list of relation attributes for this model.
644
	 *
645
	 * @return array
646
	 */
647
	public function relationAttributes()
648
	{
649
		return array_keys($this->relations);
650
	}
651
652
	/**
653
	 * Determine whether the given attribute is a relation.
654
	 *
655
	 * @param string $attribute
656
	 * @return bool
657
	 */
658
	protected function hasRelation($attribute)
659
	{
660
		$attribute = $this->prepareAttribute($attribute);
661
662
		return isset($this->relations[$attribute]);
663
	}
664
665
	/**
666
	 * Retrieve the given relation.
667
	 *
668
	 * @param string $attribute
669
	 * @return Relation
670
	 */
671
	public function relation($attribute)
672
	{
673
		if (!$this->hasRelation($attribute)) {
674
			return null;
675
		}
676
677
		$attribute = $this->prepareAttribute($attribute);
678
		$relation = $this->relations[$attribute];
679
680
		if (!$relation instanceof Relation) {
681
			$type = array_shift($relation);
682
			$arguments = array_merge(array($this), $relation);
683
			$arguments['name'] = $attribute;
684
685
			$relation = Relation::factory($type, $arguments);
686
687
			$this->relations[$attribute] = $relation;
688
		}
689
690
		return $relation;
691
	}
692
693
	/**
694
	 * Retrieve all relations.
695
	 *
696
	 * @return Relation[]
697
	 */
698
	public function relations()
699
	{
700
		$relations = array();
701
702
		foreach ($this->relationAttributes() as $attribute) {
703
			$relations[$attribute] = $this->relation($attribute);
704
		}
705
706
		return $relations;
707
	}
708
709
	/**
710
	 * Determine whether the given relation has any set models.
711
	 *
712
	 * @param string $attribute
713
	 * @return bool
714
	 */
715
	protected function hasRelated($attribute)
716
	{
717
		$attribute = $this->prepareAttribute($attribute);
718
719
		return $this->hasRelation($attribute) && $this->relation($attribute)->count();
720
	}
721
722
	/**
723
	 * Retrieve the models of the given relation.
724
	 *
725
	 * @param string $attribute
726
	 * @return array
727
	 */
728
	public function related($attribute)
729
	{
730
		if (!$this->hasRelation($attribute)) {
731
			return null;
732
		}
733
734
		$attribute = $this->prepareAttribute($attribute);
735
736
		$relation = $this->relation($attribute);
737
738
		return $relation->retrieve();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $relation->retrieve() also could return the type Darya\ORM\Record which is incompatible with the documented return type array.
Loading history...
739
	}
740
741
	/**
742
	 * Set the given related models.
743
	 *
744
	 * @param string $attribute
745
	 * @param mixed  $value
746
	 */
747
	protected function setRelated($attribute, $value)
748
	{
749
		if (!$this->hasRelation($attribute)) {
750
			return;
751
		}
752
753
		$relation = $this->relation($attribute);
754
755
		$relation->detach();
756
757
		if ($value === null) {
758
			return;
759
		}
760
761
		$relation->attach($value);
762
	}
763
764
	/**
765
	 * Unset the models of the given relation.
766
	 *
767
	 * @param string $attribute
768
	 */
769
	protected function unsetRelated($attribute)
770
	{
771
		if (!$this->hasRelation($attribute)) {
772
			return;
773
		}
774
775
		$this->relation($attribute)->detach();
776
	}
777
778
	/**
779
	 * Save all of the model's relations.
780
	 */
781
	public function saveRelations()
782
	{
783
		foreach ($this->relations() as $relation) {
784
			$relation->save();
785
		}
786
	}
787
788
	/**
789
	 * Retrieve the default search attributes for the model.
790
	 *
791
	 * @return array
792
	 */
793
	public function defaultSearchAttributes()
794
	{
795
		return $this->search;
796
	}
797
798
	/**
799
	 * Retrieve a relation. Shortcut for `relation()`.
800
	 *
801
	 * @param string $method
802
	 * @param array  $arguments
803
	 * @return Relation
804
	 * @throws Exception
805
	 */
806
	public function __call($method, $arguments)
807
	{
808
		if ($this->hasRelation($method)) {
809
			return $this->relation($method);
810
		}
811
812
		throw new Exception('Call to undefined method ' . get_class($this) . '::' . $method . '()');
813
	}
814
}
815