Completed
Push — master ( 63ed3e...5fffd6 )
by Jared
02:04
created

Model::refreshWith()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 2
nop 1
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use ArrayAccess;
15
use BadMethodCallException;
16
use Carbon\Carbon;
17
use ICanBoogie\Inflector;
18
use Infuse\Locale;
19
use InvalidArgumentException;
20
use Pulsar\Adapter\AdapterInterface;
21
use Pulsar\Exception\AdapterMissingException;
22
use Pulsar\Exception\MassAssignmentException;
23
use Pulsar\Exception\NotFoundException;
24
use Pulsar\Relation\HasOne;
25
use Pulsar\Relation\BelongsTo;
26
use Pulsar\Relation\HasMany;
27
use Pulsar\Relation\BelongsToMany;
28
use Symfony\Component\EventDispatcher\EventDispatcher;
29
30
abstract class Model implements ArrayAccess
31
{
32
    const TYPE_STRING = 'string';
33
    const TYPE_INTEGER = 'integer';
34
    const TYPE_FLOAT = 'float';
35
    const TYPE_BOOLEAN = 'boolean';
36
    const TYPE_DATE = 'date';
37
    const TYPE_OBJECT = 'object';
38
    const TYPE_ARRAY = 'array';
39
40
    const DEFAULT_ID_PROPERTY = 'id';
41
42
    const DEFAULT_DATE_FORMAT = 'U'; // unix timestamps
43
44
    // DEPRECATED
45
    const TYPE_NUMBER = 'float';
46
    const IMMUTABLE = 0;
47
    const MUTABLE_CREATE_ONLY = 1;
48
    const MUTABLE = 2;
49
50
    /////////////////////////////
51
    // Model visible variables
52
    /////////////////////////////
53
54
    /**
55
     * List of model ID property names.
56
     *
57
     * @staticvar array
58
     */
59
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
60
61
    /**
62
     * Validation rules expressed as a key-value map with
63
     * property names as the keys.
64
     * i.e. ['name' => 'string:2'].
65
     *
66
     * @staticvar array
67
     */
68
    protected static $validations = [];
69
70
    /**
71
     * @staticvar array
72
     */
73
    protected static $relationships = [];
74
75
    /**
76
     * @staticvar array
77
     */
78
    protected static $relationshipsDeprecated = [];
79
80
    /**
81
     * @staticvar array
82
     */
83
    protected static $dates = [];
84
85
    /**
86
     * @staticvar array
87
     */
88
    protected static $dispatchers = [];
89
90
    /**
91
     * @var array
92
     */
93
    protected $_values = [];
94
95
    /**
96
     * @var array
97
     */
98
    protected $_unsaved = [];
99
100
    /**
101
     * @var bool
102
     */
103
    protected $_persisted = false;
104
105
    /**
106
     * @var Errors
107
     */
108
    protected $_errors;
109
110
    /**
111
     * @deprecated
112
     *
113
     * @var array
114
     */
115
    protected $_relationships = [];
116
117
    /////////////////////////////
118
    // Base model variables
119
    /////////////////////////////
120
121
    /**
122
     * @staticvar array
123
     */
124
    private static $initialized = [];
125
126
    /**
127
     * @staticvar AdapterInterface
128
     */
129
    private static $adapter;
130
131
    /**
132
     * @staticvar Locale
133
     */
134
    private static $locale;
135
136
    /**
137
     * @staticvar array
138
     */
139
    private static $tablenames = [];
140
141
    /**
142
     * @staticvar array
143
     */
144
    private static $accessors = [];
145
146
    /**
147
     * @staticvar array
148
     */
149
    private static $mutators = [];
150
151
    /**
152
     * @var bool
153
     */
154
    private $_ignoreUnsaved;
155
156
    /**
157
     * Creates a new model object.
158
     *
159
     * @param array $values values to fill model with
160
     */
161
    public function __construct(array $values = [])
162
    {
163
        // parse deprecated property definitions
164
        if (property_exists($this, 'properties')) {
165
            $this->setDefaultValuesDeprecated();
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Model::setDefaultValuesDeprecated() has been deprecated with message: Sets the default values from a deprecated $properties format

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
166
        }
167
168
        foreach ($values as $k => $v) {
169
            $this->setValue($k, $v, false);
170
        }
171
172
        // ensure the initialize function is called only once
173
        $k = get_called_class();
174
        if (!isset(self::$initialized[$k])) {
175
            $this->initialize();
176
            self::$initialized[$k] = true;
177
        }
178
    }
179
180
    /**
181
     * @deprecated
182
     * Sets the default values from a deprecated $properties format
183
     *
184
     * @return self
185
     */
186
    private function setDefaultValuesDeprecated()
187
    {
188
        foreach (static::$properties as $k => $definition) {
189
            if (isset($definition['default'])) {
190
                $this->setValue($k, $definition['default'], false);
191
            }
192
        }
193
194
        return $this;
195
    }
196
197
    /**
198
     * The initialize() method is called once per model. It's used
199
     * to perform any one-off tasks before the model gets
200
     * constructed. This is a great place to add any model
201
     * properties. When extending this method be sure to call
202
     * parent::initialize() as some important stuff happens here.
203
     * If extending this method to add properties then you should
204
     * call parent::initialize() after adding any properties.
205
     */
206
    protected function initialize()
207
    {
208
        // parse deprecated property definitions
209
        if (property_exists($this, 'properties')) {
210
            $this->parseDeprecatedProperties();
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Model::parseDeprecatedProperties() has been deprecated with message: Parses a deprecated $properties format

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
211
        }
212
213
        // add in the default ID property
214
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
215
            if (property_exists($this, 'casts') && !isset(static::$casts[self::DEFAULT_ID_PROPERTY])) {
216
                static::$casts[self::DEFAULT_ID_PROPERTY] = self::TYPE_INTEGER;
217
            }
218
        }
219
220
        // generates created_at and updated_at timestamps
221
        if (property_exists($this, 'autoTimestamps')) {
222
            $this->installAutoTimestamps();
223
        }
224
    }
225
226
    /**
227
     * @deprecated
228
     * Parses a deprecated $properties format
229
     *
230
     * @return self
231
     */
232
    private function parseDeprecatedProperties()
233
    {
234
        foreach (static::$properties as $k => $definition) {
235
            // parse property types
236
            if (isset($definition['type'])) {
237
                static::$casts[$k] = $definition['type'];
238
            }
239
240
            // parse validations
241
            $validation = [];
242
            if (isset($definition['required'])) {
243
                $validation[] = 'required';
244
            }
245
246
            if (isset($definition['validate'])) {
247
                $validation[] = $definition['validate'];
248
            }
249
250
            if (isset($definition['unique'])) {
251
                $validation[] = 'unique';
252
            }
253
254
            if ($validation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $validation 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...
255
                static::$validations[$k] = implode('|', $validation);
256
            }
257
258
            // parse date formats
259
            if (property_exists($this, 'autoTimestamps')) {
260
                static::$dates['created_at'] = 'Y-m-d H:i:s';
261
                static::$dates['updated_at'] = 'Y-m-d H:i:s';
262
            }
263
264
            // parse deprecated relationships
265
            if (isset($definition['relation'])) {
266
                static::$relationshipsDeprecated[$k] = $definition['relation'];
267
            }
268
269
            // parse protected properties
270
            if (isset($definition['mutable']) && in_array($definition['mutable'], [self::IMMUTABLE, self::MUTABLE_CREATE_ONLY])) {
271
                static::$protected[] = $k;
272
            }
273
        }
274
275
        return $this;
276
    }
277
278
    /**
279
     * Installs the automatic timestamp properties,
280
     * `created_at` and `updated_at`.
281
     */
282
    private function installAutoTimestamps()
283
    {
284
        if (property_exists($this, 'casts')) {
285
            static::$casts['created_at'] = self::TYPE_DATE;
286
            static::$casts['updated_at'] = self::TYPE_DATE;
287
        }
288
289
        self::creating(function (ModelEvent $event) {
290
            $model = $event->getModel();
291
            $model->created_at = Carbon::now();
0 ignored issues
show
Documentation introduced by
The property created_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
292
            $model->updated_at = Carbon::now();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
293
        });
294
295
        self::updating(function (ModelEvent $event) {
296
            $event->getModel()->updated_at = Carbon::now();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
297
        });
298
    }
299
300
    /**
301
     * Sets the adapter for all models.
302
     *
303
     * @param AdapterInterface $adapter
304
     */
305
    public static function setAdapter(AdapterInterface $adapter)
306
    {
307
        self::$adapter = $adapter;
308
    }
309
310
    /**
311
     * Gets the adapter for all models.
312
     *
313
     * @return AdapterInterface
314
     *
315
     * @throws AdapterMissingException
316
     */
317
    public static function getAdapter()
318
    {
319
        if (!self::$adapter) {
320
            throw new AdapterMissingException('A model adapter has not been set yet.');
321
        }
322
323
        return self::$adapter;
324
    }
325
326
    /**
327
     * Clears the adapter for all models.
328
     */
329
    public static function clearAdapter()
330
    {
331
        self::$adapter = null;
332
    }
333
334
    /**
335
     * @deprecated
336
     */
337
    public static function setDriver(AdapterInterface $adapter)
338
    {
339
        self::$adapter = $adapter;
340
    }
341
342
    /**
343
     * @deprecated
344
     */
345
    public static function getDriver()
346
    {
347
        if (!self::$adapter) {
348
            throw new AdapterMissingException('A model adapter has not been set yet.');
349
        }
350
351
        return self::$adapter;
352
    }
353
354
    /**
355
     * @deprecated
356
     */
357
    public static function clearDriver()
358
    {
359
        self::$adapter = null;
360
    }
361
362
    /**
363
     * Sets the locale instance for all models.
364
     *
365
     * @param Locale $locale
366
     */
367
    public static function setLocale(Locale $locale)
368
    {
369
        self::$locale = $locale;
370
    }
371
372
    /**
373
     * Gets the locale instance for all models.
374
     *
375
     * @return Locale
376
     */
377
    public static function getLocale()
378
    {
379
        return self::$locale;
380
    }
381
382
    /**
383
     * Clears the locale for all models.
384
     */
385
    public static function clearLocale()
386
    {
387
        self::$locale = null;
388
    }
389
390
    /**
391
     * Gets the name of the model without namespacing.
392
     *
393
     * @return string
394
     */
395
    public static function modelName()
396
    {
397
        return explode('\\', get_called_class())[0];
398
    }
399
400
    /**
401
     * Gets the table name of the model.
402
     *
403
     * @return string
404
     */
405
    public function getTablename()
406
    {
407
        $name = static::modelName();
408
        if (!isset(self::$tablenames[$name])) {
409
            $inflector = Inflector::get();
410
411
            self::$tablenames[$name] = $inflector->camelize($inflector->pluralize($name));
412
        }
413
414
        return self::$tablenames[$name];
415
    }
416
417
    /**
418
     * Gets the model ID.
419
     *
420
     * @return string|number|null ID
421
     */
422
    public function id()
423
    {
424
        $ids = $this->ids();
425
426
        // if a single ID then return it
427
        if (count($ids) === 1) {
428
            return reset($ids);
429
        }
430
431
        // if multiple IDs then return a comma-separated list
432
        return implode(',', $ids);
433
    }
434
435
    /**
436
     * Gets a key-value map of the model ID.
437
     *
438
     * @return array ID map
439
     */
440
    public function ids()
441
    {
442
        return $this->getValues(static::$ids);
443
    }
444
445
    /////////////////////////////
446
    // Magic Methods
447
    /////////////////////////////
448
449
    public function __toString()
450
    {
451
        return get_called_class().'('.$this->id().')';
452
    }
453
454
    public function __get($name)
455
    {
456
        $value = $this->getValue($name);
457
        $this->_ignoreUnsaved = false;
458
459
        return $value;
460
    }
461
462
    public function __set($name, $value)
463
    {
464
        $this->setValue($name, $value);
465
    }
466
467
    public function __isset($name)
468
    {
469
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
470
    }
471
472
    public function __unset($name)
473
    {
474
        if (static::isRelationship($name)) {
475
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
476
        }
477
478
        if (array_key_exists($name, $this->_unsaved)) {
479
            unset($this->_unsaved[$name]);
480
        }
481
482
        // if changing property, remove relation model
483
        // DEPRECATED
484
        if (isset($this->_relationships[$name])) {
0 ignored issues
show
Deprecated Code introduced by
The property Pulsar\Model::$_relationships has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
485
            unset($this->_relationships[$name]);
486
        }
487
    }
488
489
    public static function __callStatic($name, $parameters)
490
    {
491
        // Any calls to unkown static methods should be deferred to
492
        // the query. This allows calls like User::where()
493
        // to replace User::query()->where().
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
494
        return call_user_func_array([static::query(), $name], $parameters);
495
    }
496
497
    /////////////////////////////
498
    // ArrayAccess Interface
499
    /////////////////////////////
500
501
    public function offsetExists($offset)
502
    {
503
        return isset($this->$offset);
504
    }
505
506
    public function offsetGet($offset)
507
    {
508
        return $this->$offset;
509
    }
510
511
    public function offsetSet($offset, $value)
512
    {
513
        $this->$offset = $value;
514
    }
515
516
    public function offsetUnset($offset)
517
    {
518
        unset($this->$offset);
519
    }
520
521
    /////////////////////////////
522
    // Property Definitions
523
    /////////////////////////////
524
525
    /**
526
     * Gets the names of the model ID properties.
527
     *
528
     * @return array
529
     */
530
    public static function getIdProperties()
531
    {
532
        return static::$ids;
533
    }
534
535
    /**
536
     * Builds an existing model instance given a single ID value or
537
     * ordered array of ID values.
538
     *
539
     * @param mixed $id
540
     *
541
     * @return Model
542
     */
543
    public static function buildFromId($id)
544
    {
545
        $ids = [];
546
        $id = (array) $id;
547
        foreach (static::$ids as $j => $k) {
548
            $ids[$k] = $id[$j];
549
        }
550
551
        $model = new static($ids);
552
553
        return $model;
554
    }
555
556
    /**
557
     * Gets the mutator method name for a given proeprty name.
558
     * Looks for methods in the form of `setPropertyValue`.
559
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
560
     *
561
     * @param string $property
562
     *
563
     * @return string|false method name if it exists
564
     */
565 View Code Duplication
    public static function getMutator($property)
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...
566
    {
567
        $class = get_called_class();
568
569
        $k = $class.':'.$property;
570
        if (!array_key_exists($k, self::$mutators)) {
571
            $inflector = Inflector::get();
572
            $method = 'set'.$inflector->camelize($property).'Value';
573
574
            if (!method_exists($class, $method)) {
575
                $method = false;
576
            }
577
578
            self::$mutators[$k] = $method;
579
        }
580
581
        return self::$mutators[$k];
582
    }
583
584
    /**
585
     * Gets the accessor method name for a given proeprty name.
586
     * Looks for methods in the form of `getPropertyValue`.
587
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
588
     *
589
     * @param string $property
590
     *
591
     * @return string|false method name if it exists
592
     */
593 View Code Duplication
    public static function getAccessor($property)
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...
594
    {
595
        $class = get_called_class();
596
597
        $k = $class.':'.$property;
598
        if (!array_key_exists($k, self::$accessors)) {
599
            $inflector = Inflector::get();
600
            $method = 'get'.$inflector->camelize($property).'Value';
601
602
            if (!method_exists($class, $method)) {
603
                $method = false;
604
            }
605
606
            self::$accessors[$k] = $method;
607
        }
608
609
        return self::$accessors[$k];
610
    }
611
612
    /**
613
     * Checks if a given property is a relationship.
614
     *
615
     * @param string $property
616
     *
617
     * @return bool
618
     */
619
    public static function isRelationship($property)
620
    {
621
        return in_array($property, static::$relationships);
622
    }
623
624
    /**
625
     * Gets the string date format for a property. Defaults to
626
     * UNIX timestamps.
627
     *
628
     * @param string $property
629
     *
630
     * @return string
631
     */
632
    public static function getDateFormat($property)
633
    {
634
        if (isset(static::$dates[$property])) {
635
            return static::$dates[$property];
636
        }
637
638
        return self::DEFAULT_DATE_FORMAT;
639
    }
640
641
    /**
642
     * Gets the title of a property.
643
     *
644
     * @param string $name
645
     *
646
     * @return string
647
     */
648
    public static function getPropertyTitle($name)
649
    {
650
        // attmept to fetch the title from the Locale service
651
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
652
        if (self::$locale && $title = self::$locale->t($k)) {
653
            if ($title != $k) {
654
                return $title;
655
            }
656
        }
657
658
        return Inflector::get()->humanize($name);
659
    }
660
661
    /**
662
     * Gets the type cast for a property.
663
     *
664
     * @param string $property
665
     *
666
     * @return string|null
667
     */
668
    public static function getPropertyType($property)
669
    {
670
        if (property_exists(get_called_class(), 'casts')) {
671
            return array_value(static::$casts, $property);
672
        }
673
    }
674
675
    /**
676
     * Casts a value to a given type.
677
     *
678
     * @param string|null $type
679
     * @param mixed       $value
680
     * @param string      $property optional property name
681
     *
682
     * @return mixed casted value
683
     */
684
    public static function cast($type, $value, $property = null)
685
    {
686
        if ($value === null) {
687
            return;
688
        }
689
690
        if ($type == self::TYPE_DATE) {
691
            $format = self::getDateFormat($property);
692
693
            return Property::to_date($value, $format);
694
        }
695
696
        $m = 'to_'.$type;
697
698
        return Property::$m($value);
699
    }
700
701
    /**
702
     * Gets the properties of this model.
703
     *
704
     * @return array
705
     */
706
    public function getProperties()
707
    {
708
        return array_unique(array_merge(
709
            static::$ids, array_keys($this->_values)));
710
    }
711
712
    /**
713
     * Checks if the model has a property.
714
     *
715
     * @param string $property
716
     *
717
     * @return bool has property
718
     */
719
    public function hasProperty($property)
720
    {
721
        return array_key_exists($property, $this->_values) ||
722
               in_array($property, static::$ids);
723
    }
724
725
    /////////////////////////////
726
    // Values
727
    /////////////////////////////
728
729
    /**
730
     * Sets an unsaved value.
731
     *
732
     * @param string $name
733
     * @param mixed  $value
734
     * @param bool   $unsaved when true, sets an unsaved value
735
     *
736
     * @throws BadMethodCallException when setting a relationship
737
     *
738
     * @return self
739
     */
740
    public function setValue($name, $value, $unsaved = true)
741
    {
742
        if (static::isRelationship($name)) {
743
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
744
        }
745
746
        // cast the value
747
        if ($type = static::getPropertyType($name)) {
748
            $value = static::cast($type, $value, $name);
749
        }
750
751
        // apply any mutators
752
        if ($mutator = self::getMutator($name)) {
753
            $value = $this->$mutator($value);
754
        }
755
756
        // save the value on the model property
757
        if ($unsaved) {
758
            $this->_unsaved[$name] = $value;
759
        } else {
760
            $this->_values[$name] = $value;
761
        }
762
763
        // if changing property, remove relation model
764
        // DEPRECATED
765
        if (isset($this->_relationships[$name])) {
0 ignored issues
show
Deprecated Code introduced by
The property Pulsar\Model::$_relationships has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
766
            unset($this->_relationships[$name]);
767
        }
768
769
        return $this;
770
    }
771
772
    /**
773
     * Sets a collection values on the model from an untrusted
774
     * input. Also known as mass assignment.
775
     *
776
     * @param array $values
777
     *
778
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
779
     *
780
     * @return self
781
     */
782
    public function setValues($values)
783
    {
784
        // check if the model has a mass assignment whitelist
785
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
786
787
        // if no whitelist, then check for a blacklist
788
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
789
790
        foreach ($values as $k => $value) {
791
            // check for mass assignment violations
792
            if (($permitted && !in_array($k, $permitted)) ||
793
                ($protected && in_array($k, $protected))) {
794
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
795
            }
796
797
            $this->setValue($k, $value);
798
        }
799
800
        return $this;
801
    }
802
803
    /**
804
     * Ignores unsaved values when fetching the next value.
805
     *
806
     * @return self
807
     */
808
    public function ignoreUnsaved()
809
    {
810
        $this->_ignoreUnsaved = true;
811
812
        return $this;
813
    }
814
815
    /**
816
     * Gets a list of property values from the model.
817
     *
818
     * @param array $properties list of property values to fetch
819
     *
820
     * @return array
821
     */
822
    public function getValues(array $properties)
823
    {
824
        $result = [];
825
        foreach ($properties as $k) {
826
            $result[$k] = $this->getValue($k);
827
        }
828
829
        $this->_ignoreUnsaved = false;
830
831
        return $result;
832
    }
833
834
    /**
835
     * @deprecated
836
     */
837
    public function get(array $properties)
838
    {
839
        return $this->getValues($properties);
840
    }
841
842
    /**
843
     * Gets a property value from the model.
844
     *
845
     * Values are looked up in this order:
846
     *  1. unsaved values
847
     *  2. local values
848
     *  3. relationships
849
     *
850
     * @throws InvalidArgumentException when a property was requested not present in the values
851
     *
852
     * @return mixed
853
     */
854
    private function getValue($property)
855
    {
856
        $value = null;
857
        $accessor = self::getAccessor($property);
858
859
        // first check for unsaved values
860
        if (!$this->_ignoreUnsaved && array_key_exists($property, $this->_unsaved)) {
861
            $value = $this->_unsaved[$property];
862
863
        // then check the normal value store
864
        } elseif (array_key_exists($property, $this->_values)) {
865
            $value = $this->_values[$property];
866
867
        // get relationship values
868
        } elseif (static::isRelationship($property)) {
869
            $value = $this->loadRelationship($property);
870
871
        // throw an exception for non-properties
872
        // that do not have an accessor
873
        } elseif ($accessor === false && !in_array($property, static::$ids)) {
874
            throw new InvalidArgumentException(static::modelName().' does not have a `'.$property.'` property.');
875
        }
876
877
        // call any accessors
878
        if ($accessor !== false) {
879
            return $this->$accessor($value);
880
        }
881
882
        return $value;
883
    }
884
885
    /**
886
     * Converts the model to an array.
887
     *
888
     * @return array model array
889
     */
890
    public function toArray()
891
    {
892
        // build the list of properties to retrieve
893
        $properties = $this->getProperties();
894
895
        // remove any hidden properties
896
        if (property_exists($this, 'hidden')) {
897
            $properties = array_diff($properties, static::$hidden);
898
        }
899
900
        // include any appended properties
901
        if (property_exists($this, 'appended')) {
902
            $properties = array_unique(array_merge($properties, static::$appended));
903
        }
904
905
        // get the values for the properties
906
        $result = $this->getValues($properties);
907
908
        foreach ($result as $k => &$value) {
909
            // convert any models to arrays
910
            if ($value instanceof self) {
911
                $value = $value->toArray();
912
            // convert any Carbon objects to date strings
913
            } elseif ($value instanceof Carbon) {
914
                $format = self::getDateFormat($k);
915
                $value = $value->format($format);
916
            }
917
        }
918
919
        return $result;
920
    }
921
922
    /////////////////////////////
923
    // Persistence
924
    /////////////////////////////
925
926
    /**
927
     * Saves the model.
928
     *
929
     * @return bool
930
     */
931
    public function save()
932
    {
933
        if (!$this->_persisted) {
934
            return $this->create();
935
        }
936
937
        return $this->set();
938
    }
939
940
    /**
941
     * Creates a new model.
942
     *
943
     * @param array $data optional key-value properties to set
944
     *
945
     * @return bool
946
     *
947
     * @throws BadMethodCallException when called on an existing model
948
     */
949
    public function create(array $data = [])
950
    {
951
        if ($this->_persisted) {
952
            throw new BadMethodCallException('Cannot call create() on an existing model');
953
        }
954
955
        // mass assign values passed into create()
956
        $this->setValues($data);
957
958
        // add in any preset values
959
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
960
961
        // dispatch the model.creating event
962
        if (!$this->dispatch(ModelEvent::CREATING)) {
963
            return false;
964
        }
965
966
        // validate the model
967
        if (!$this->valid()) {
968
            return false;
969
        }
970
971
        // persist the model in the data layer
972
        if (!self::getAdapter()->createModel($this, $this->_unsaved)) {
973
            return false;
974
        }
975
976
        // update the model with the persisted values and new ID(s)
977
        $newValues = array_replace(
978
            $this->_unsaved,
979
            $this->getNewIds());
980
        $this->refreshWith($newValues);
981
982
        // dispatch the model.created event
983
        return $this->dispatch(ModelEvent::CREATED);
984
    }
985
986
    /**
987
     * Gets the IDs for a newly created model.
988
     *
989
     * @return string
990
     */
991
    protected function getNewIds()
992
    {
993
        $ids = [];
994
        foreach (static::$ids as $k) {
995
            // check if the ID property was already given,
996
            if (isset($this->_unsaved[$k])) {
997
                $ids[$k] = $this->_unsaved[$k];
998
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
999
            } else {
1000
                $ids[$k] = self::getAdapter()->getCreatedID($this, $k);
1001
            }
1002
        }
1003
1004
        return $ids;
1005
    }
1006
1007
    /**
1008
     * Updates the model.
1009
     *
1010
     * @param array $data optional key-value properties to set
1011
     *
1012
     * @return bool
1013
     *
1014
     * @throws BadMethodCallException when not called on an existing model
1015
     */
1016
    public function set(array $data = [])
1017
    {
1018
        if (!$this->_persisted) {
1019
            throw new BadMethodCallException('Can only call set() on an existing model');
1020
        }
1021
1022
        // mass assign values passed into set()
1023
        $this->setValues($data);
1024
1025
        // not updating anything?
1026
        if (count($this->_unsaved) === 0) {
1027
            return true;
1028
        }
1029
1030
        // dispatch the model.updating event
1031
        if (!$this->dispatch(ModelEvent::UPDATING)) {
1032
            return false;
1033
        }
1034
1035
        // validate the model
1036
        if (!$this->valid()) {
1037
            return false;
1038
        }
1039
1040
        // persist the model in the data layer
1041
        if (!self::getAdapter()->updateModel($this, $this->_unsaved)) {
1042
            return false;
1043
        }
1044
1045
        // update the model with the persisted values
1046
        $this->refreshWith($this->_unsaved);
1047
1048
        // dispatch the model.updated event
1049
        return $this->dispatch(ModelEvent::UPDATED);
1050
    }
1051
1052
    /**
1053
     * Delete the model.
1054
     *
1055
     * @return bool success
1056
     */
1057
    public function delete()
1058
    {
1059
        if (!$this->_persisted) {
1060
            throw new BadMethodCallException('Can only call delete() on an existing model');
1061
        }
1062
1063
        // dispatch the model.deleting event
1064
        if (!$this->dispatch(ModelEvent::DELETING)) {
1065
            return false;
1066
        }
1067
1068
        // delete the model in the data layer
1069
        if (!self::getAdapter()->deleteModel($this)) {
1070
            return false;
1071
        }
1072
1073
        // dispatch the model.deleted event
1074
        if (!$this->dispatch(ModelEvent::DELETED)) {
1075
            return false;
1076
        }
1077
1078
        $this->_persisted = false;
1079
1080
        return true;
1081
    }
1082
1083
    /**
1084
     * Tells if the model has been persisted.
1085
     *
1086
     * @return bool
1087
     */
1088
    public function persisted()
1089
    {
1090
        return $this->_persisted;
1091
    }
1092
1093
    /**
1094
     * Loads the model from the data layer.
1095
     *
1096
     * @return self
1097
     *
1098
     * @throws NotFoundException
1099
     */
1100
    public function refresh()
1101
    {
1102
        if (!$this->_persisted) {
1103
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
1104
        }
1105
1106
        $query = static::query();
1107
        $query->where($this->ids());
1108
1109
        $values = self::getAdapter()->queryModels($query);
1110
1111
        if (count($values) === 0) {
1112
            return $this;
1113
        }
1114
1115
        // clear any relations
1116
        // DEPRECATED
1117
        $this->_relationships = [];
0 ignored issues
show
Deprecated Code introduced by
The property Pulsar\Model::$_relationships has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1118
1119
        return $this->refreshWith($values[0]);
1120
    }
1121
1122
    /**
1123
     * Loads values into the model retrieved from the data layer.
1124
     *
1125
     * @param array $values values
1126
     *
1127
     * @return self
1128
     */
1129
    public function refreshWith(array $values)
1130
    {
1131
        // cast the values
1132
        if (property_exists($this, 'casts')) {
1133
            foreach ($values as $k => &$value) {
1134
                if ($type = static::getPropertyType($k)) {
1135
                    $value = static::cast($type, $value, $k);
1136
                }
1137
            }
1138
        }
1139
1140
        $this->_persisted = true;
1141
        $this->_values = $values;
1142
        $this->_unsaved = [];
1143
1144
        return $this;
1145
    }
1146
1147
    /////////////////////////////
1148
    // Queries
1149
    /////////////////////////////
1150
1151
    /**
1152
     * Generates a new query instance.
1153
     *
1154
     * @return Query
1155
     */
1156
    public static function query()
1157
    {
1158
        // Create a new model instance for the query to ensure
1159
        // that the model's initialize() method gets called.
1160
        // Otherwise, the property definitions will be incomplete.
1161
        $model = new static();
1162
1163
        return new Query($model);
1164
    }
1165
1166
    /**
1167
     * Finds a single instance of a model given it's ID.
1168
     *
1169
     * @param mixed $id
1170
     *
1171
     * @return Model|null
1172
     */
1173
    public static function find($id)
1174
    {
1175
        $model = static::buildFromId($id);
1176
1177
        return static::query()->where($model->ids())->first();
1178
    }
1179
1180
    /**
1181
     * Finds a single instance of a model given it's ID or throws an exception.
1182
     *
1183
     * @param mixed $id
1184
     *
1185
     * @return Model|false
1186
     *
1187
     * @throws NotFoundException when a model could not be found
1188
     */
1189
    public static function findOrFail($id)
1190
    {
1191
        $model = static::find($id);
1192
        if (!$model) {
1193
            throw new NotFoundException('Could not find the requested '.static::modelName());
1194
        }
1195
1196
        return $model;
1197
    }
1198
1199
    /**
1200
     * Gets the toal number of records matching an optional criteria.
1201
     *
1202
     * @param array $where criteria
1203
     *
1204
     * @return int total
1205
     */
1206
    public static function totalRecords(array $where = [])
1207
    {
1208
        $query = static::query();
1209
        $query->where($where);
1210
1211
        return self::getAdapter()->totalRecords($query);
1212
    }
1213
1214
    /////////////////////////////
1215
    // Relationships
1216
    /////////////////////////////
1217
1218
    /**
1219
     * Creates the parent side of a One-To-One relationship.
1220
     *
1221
     * @param string $model      foreign model class
1222
     * @param string $foreignKey identifying key on foreign model
1223
     * @param string $localKey   identifying key on local model
1224
     *
1225
     * @return \Pulsar\Relation\Relation
1226
     */
1227
    public function hasOne($model, $foreignKey = '', $localKey = '')
1228
    {
1229
        return new HasOne($this, $localKey, $model, $foreignKey);
1230
    }
1231
1232
    /**
1233
     * Creates the child side of a One-To-One or One-To-Many relationship.
1234
     *
1235
     * @param string $model      foreign model class
1236
     * @param string $foreignKey identifying key on foreign model
1237
     * @param string $localKey   identifying key on local model
1238
     *
1239
     * @return \Pulsar\Relation\Relation
1240
     */
1241
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1242
    {
1243
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1244
    }
1245
1246
    /**
1247
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1248
     *
1249
     * @param string $model      foreign model class
1250
     * @param string $foreignKey identifying key on foreign model
1251
     * @param string $localKey   identifying key on local model
1252
     *
1253
     * @return \Pulsar\Relation\Relation
1254
     */
1255
    public function hasMany($model, $foreignKey = '', $localKey = '')
1256
    {
1257
        return new HasMany($this, $localKey, $model, $foreignKey);
1258
    }
1259
1260
    /**
1261
     * Creates the child side of a Many-To-Many relationship.
1262
     *
1263
     * @param string $model      foreign model class
1264
     * @param string $tablename  pivot table name
1265
     * @param string $foreignKey identifying key on foreign model
1266
     * @param string $localKey   identifying key on local model
1267
     *
1268
     * @return \Pulsar\Relation\Relation
1269
     */
1270
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1271
    {
1272
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1273
    }
1274
1275
    /**
1276
     * Loads a given relationship (if not already) and
1277
     * returns its results.
1278
     *
1279
     * @param string $name
1280
     *
1281
     * @return mixed
1282
     */
1283
    protected function loadRelationship($name)
1284
    {
1285
        if (!isset($this->_values[$name])) {
1286
            $relationship = $this->$name();
1287
            $this->_values[$name] = $relationship->getResults();
1288
        }
1289
1290
        return $this->_values[$name];
1291
    }
1292
1293
    /**
1294
     * @deprecated
1295
     * Gets a relationship model with a has one relationship
1296
     *
1297
     * @param string $k property
1298
     *
1299
     * @return \Pulsar\Model|null
1300
     */
1301
    public function relation($k)
1302
    {
1303
        if (!isset(static::$relationshipsDeprecated[$k])) {
1304
            return;
1305
        }
1306
1307
        if (!isset($this->_relationships[$k])) {
0 ignored issues
show
Deprecated Code introduced by
The property Pulsar\Model::$_relationships has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1308
            $model = static::$relationshipsDeprecated[$k];
1309
            $this->_relationships[$k] = $model::find($this->$k);
0 ignored issues
show
Deprecated Code introduced by
The property Pulsar\Model::$_relationships has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1310
        }
1311
1312
        return $this->_relationships[$k];
0 ignored issues
show
Deprecated Code introduced by
The property Pulsar\Model::$_relationships has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1313
    }
1314
1315
    /////////////////////////////
1316
    // Events
1317
    /////////////////////////////
1318
1319
    /**
1320
     * Gets the event dispatcher.
1321
     *
1322
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1323
     */
1324
    public static function getDispatcher($ignoreCache = false)
1325
    {
1326
        $class = get_called_class();
1327
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1328
            self::$dispatchers[$class] = new EventDispatcher();
1329
        }
1330
1331
        return self::$dispatchers[$class];
1332
    }
1333
1334
    /**
1335
     * Subscribes to a listener to an event.
1336
     *
1337
     * @param string   $event    event name
1338
     * @param callable $listener
1339
     * @param int      $priority optional priority, higher #s get called first
1340
     */
1341
    public static function listen($event, callable $listener, $priority = 0)
1342
    {
1343
        static::getDispatcher()->addListener($event, $listener, $priority);
1344
    }
1345
1346
    /**
1347
     * Adds a listener to the model.creating event.
1348
     *
1349
     * @param callable $listener
1350
     * @param int      $priority
1351
     */
1352
    public static function creating(callable $listener, $priority = 0)
1353
    {
1354
        static::listen(ModelEvent::CREATING, $listener, $priority);
1355
    }
1356
1357
    /**
1358
     * Adds a listener to the model.created event.
1359
     *
1360
     * @param callable $listener
1361
     * @param int      $priority
1362
     */
1363
    public static function created(callable $listener, $priority = 0)
1364
    {
1365
        static::listen(ModelEvent::CREATED, $listener, $priority);
1366
    }
1367
1368
    /**
1369
     * Adds a listener to the model.updating event.
1370
     *
1371
     * @param callable $listener
1372
     * @param int      $priority
1373
     */
1374
    public static function updating(callable $listener, $priority = 0)
1375
    {
1376
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1377
    }
1378
1379
    /**
1380
     * Adds a listener to the model.updated event.
1381
     *
1382
     * @param callable $listener
1383
     * @param int      $priority
1384
     */
1385
    public static function updated(callable $listener, $priority = 0)
1386
    {
1387
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1388
    }
1389
1390
    /**
1391
     * Adds a listener to the model.creating and model.updating events.
1392
     *
1393
     * @param callable $listener
1394
     * @param int      $priority
1395
     */
1396
    public static function saving(callable $listener, $priority = 0)
1397
    {
1398
        static::listen(ModelEvent::CREATING, $listener, $priority);
1399
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1400
    }
1401
1402
    /**
1403
     * Adds a listener to the model.created and model.updated events.
1404
     *
1405
     * @param callable $listener
1406
     * @param int      $priority
1407
     */
1408
    public static function saved(callable $listener, $priority = 0)
1409
    {
1410
        static::listen(ModelEvent::CREATED, $listener, $priority);
1411
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1412
    }
1413
1414
    /**
1415
     * Adds a listener to the model.deleting event.
1416
     *
1417
     * @param callable $listener
1418
     * @param int      $priority
1419
     */
1420
    public static function deleting(callable $listener, $priority = 0)
1421
    {
1422
        static::listen(ModelEvent::DELETING, $listener, $priority);
1423
    }
1424
1425
    /**
1426
     * Adds a listener to the model.deleted event.
1427
     *
1428
     * @param callable $listener
1429
     * @param int      $priority
1430
     */
1431
    public static function deleted(callable $listener, $priority = 0)
1432
    {
1433
        static::listen(ModelEvent::DELETED, $listener, $priority);
1434
    }
1435
1436
    /**
1437
     * Dispatches an event.
1438
     *
1439
     * @param string $eventName
1440
     *
1441
     * @return bool true when the event propagated fully without being stopped
1442
     */
1443
    protected function dispatch($eventName)
1444
    {
1445
        $event = new ModelEvent($this);
1446
1447
        static::getDispatcher()->dispatch($eventName, $event);
1448
1449
        return !$event->isPropagationStopped();
1450
    }
1451
1452
    /////////////////////////////
1453
    // Validation
1454
    /////////////////////////////
1455
1456
    /**
1457
     * Gets the error stack for this model instance. Used to
1458
     * keep track of validation errors.
1459
     *
1460
     * @return Errors
1461
     */
1462
    public function errors()
1463
    {
1464
        if (!$this->_errors) {
1465
            $this->_errors = new Errors($this, self::$locale);
1466
        }
1467
1468
        return $this->_errors;
1469
    }
1470
1471
    /**
1472
     * Checks if the model is valid in its current state.
1473
     *
1474
     * @return bool
1475
     */
1476
    public function valid()
1477
    {
1478
        // clear any previous errors
1479
        $this->errors()->clear();
1480
1481
        // run the validator against the model values
1482
        $validator = $this->getValidator();
1483
        $values = $this->_unsaved + $this->_values;
1484
        $validated = $validator->validate($values);
1485
1486
        // add back any modified unsaved values
1487
        foreach (array_keys($this->_unsaved) as $k) {
1488
            $this->_unsaved[$k] = $values[$k];
1489
        }
1490
1491
        return $validated;
1492
    }
1493
1494
    /**
1495
     * Gets a new validator instance for this model.
1496
     *
1497
     * @return Validator
1498
     */
1499
    public function getValidator()
1500
    {
1501
        return new Validator(static::$validations, $this->errors());
1502
    }
1503
}
1504