Completed
Push — master ( 6982c8...e6f124 )
by Jared
02:05
created

Model::parseDeprecatedProperties()   D

Complexity

Conditions 9
Paths 129

Size

Total Lines 40
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 4.6666
c 0
b 0
f 0
cc 9
eloc 19
nc 129
nop 0
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'; // DEPRECATED
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
270
        return $this;
271
    }
272
273
    /**
274
     * Installs the automatic timestamp properties,
275
     * `created_at` and `updated_at`.
276
     */
277
    private function installAutoTimestamps()
278
    {
279
        if (property_exists($this, 'casts')) {
280
            static::$casts['created_at'] = self::TYPE_DATE;
281
            static::$casts['updated_at'] = self::TYPE_DATE;
282
        }
283
284
        self::creating(function (ModelEvent $event) {
285
            $model = $event->getModel();
286
            $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...
287
            $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...
288
        });
289
290
        self::updating(function (ModelEvent $event) {
291
            $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...
292
        });
293
    }
294
295
    /**
296
     * Sets the adapter for all models.
297
     *
298
     * @param AdapterInterface $adapter
299
     */
300
    public static function setAdapter(AdapterInterface $adapter)
301
    {
302
        self::$adapter = $adapter;
303
    }
304
305
    /**
306
     * Gets the adapter for all models.
307
     *
308
     * @return AdapterInterface
309
     *
310
     * @throws AdapterMissingException
311
     */
312
    public static function getAdapter()
313
    {
314
        if (!self::$adapter) {
315
            throw new AdapterMissingException('A model adapter has not been set yet.');
316
        }
317
318
        return self::$adapter;
319
    }
320
321
    /**
322
     * Clears the adapter for all models.
323
     */
324
    public static function clearAdapter()
325
    {
326
        self::$adapter = null;
327
    }
328
329
    /**
330
     * Sets the locale instance for all models.
331
     *
332
     * @param Locale $locale
333
     */
334
    public static function setLocale(Locale $locale)
335
    {
336
        self::$locale = $locale;
337
    }
338
339
    /**
340
     * Gets the locale instance for all models.
341
     *
342
     * @return Locale
343
     */
344
    public static function getLocale()
345
    {
346
        return self::$locale;
347
    }
348
349
    /**
350
     * Clears the locale for all models.
351
     */
352
    public static function clearLocale()
353
    {
354
        self::$locale = null;
355
    }
356
357
    /**
358
     * Gets the name of the model without namespacing.
359
     *
360
     * @return string
361
     */
362
    public static function modelName()
363
    {
364
        return explode('\\', get_called_class())[0];
365
    }
366
367
    /**
368
     * Gets the table name of the model.
369
     *
370
     * @return string
371
     */
372
    public function getTablename()
373
    {
374
        $name = static::modelName();
375
        if (!isset(self::$tablenames[$name])) {
376
            $inflector = Inflector::get();
377
378
            self::$tablenames[$name] = $inflector->camelize($inflector->pluralize($name));
379
        }
380
381
        return self::$tablenames[$name];
382
    }
383
384
    /**
385
     * Gets the model ID.
386
     *
387
     * @return string|number|null ID
388
     */
389
    public function id()
390
    {
391
        $ids = $this->ids();
392
393
        // if a single ID then return it
394
        if (count($ids) === 1) {
395
            return reset($ids);
396
        }
397
398
        // if multiple IDs then return a comma-separated list
399
        return implode(',', $ids);
400
    }
401
402
    /**
403
     * Gets a key-value map of the model ID.
404
     *
405
     * @return array ID map
406
     */
407
    public function ids()
408
    {
409
        return $this->getValues(static::$ids);
410
    }
411
412
    /////////////////////////////
413
    // Magic Methods
414
    /////////////////////////////
415
416
    public function __toString()
417
    {
418
        return get_called_class().'('.$this->id().')';
419
    }
420
421
    public function __get($name)
422
    {
423
        $value = $this->getValue($name);
424
        $this->_ignoreUnsaved = false;
425
426
        return $value;
427
    }
428
429
    public function __set($name, $value)
430
    {
431
        $this->setValue($name, $value);
432
    }
433
434
    public function __isset($name)
435
    {
436
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
437
    }
438
439
    public function __unset($name)
440
    {
441
        if (static::isRelationship($name)) {
442
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
443
        }
444
445
        if (array_key_exists($name, $this->_unsaved)) {
446
            unset($this->_unsaved[$name]);
447
        }
448
449
        // if changing property, remove relation model
450
        // DEPRECATED
451
        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...
452
            unset($this->_relationships[$name]);
453
        }
454
    }
455
456
    public static function __callStatic($name, $parameters)
457
    {
458
        // Any calls to unkown static methods should be deferred to
459
        // the query. This allows calls like User::where()
460
        // 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...
461
        return call_user_func_array([static::query(), $name], $parameters);
462
    }
463
464
    /////////////////////////////
465
    // ArrayAccess Interface
466
    /////////////////////////////
467
468
    public function offsetExists($offset)
469
    {
470
        return isset($this->$offset);
471
    }
472
473
    public function offsetGet($offset)
474
    {
475
        return $this->$offset;
476
    }
477
478
    public function offsetSet($offset, $value)
479
    {
480
        $this->$offset = $value;
481
    }
482
483
    public function offsetUnset($offset)
484
    {
485
        unset($this->$offset);
486
    }
487
488
    /////////////////////////////
489
    // Property Definitions
490
    /////////////////////////////
491
492
    /**
493
     * Gets the names of the model ID properties.
494
     *
495
     * @return array
496
     */
497
    public static function getIdProperties()
498
    {
499
        return static::$ids;
500
    }
501
502
    /**
503
     * Builds an existing model instance given a single ID value or
504
     * ordered array of ID values.
505
     *
506
     * @param mixed $id
507
     *
508
     * @return Model
509
     */
510
    public static function buildFromId($id)
511
    {
512
        $ids = [];
513
        $id = (array) $id;
514
        foreach (static::$ids as $j => $k) {
515
            $ids[$k] = $id[$j];
516
        }
517
518
        $model = new static($ids);
519
520
        return $model;
521
    }
522
523
    /**
524
     * Gets the mutator method name for a given proeprty name.
525
     * Looks for methods in the form of `setPropertyValue`.
526
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
527
     *
528
     * @param string $property
529
     *
530
     * @return string|false method name if it exists
531
     */
532 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...
533
    {
534
        $class = get_called_class();
535
536
        $k = $class.':'.$property;
537
        if (!array_key_exists($k, self::$mutators)) {
538
            $inflector = Inflector::get();
539
            $method = 'set'.$inflector->camelize($property).'Value';
540
541
            if (!method_exists($class, $method)) {
542
                $method = false;
543
            }
544
545
            self::$mutators[$k] = $method;
546
        }
547
548
        return self::$mutators[$k];
549
    }
550
551
    /**
552
     * Gets the accessor method name for a given proeprty name.
553
     * Looks for methods in the form of `getPropertyValue`.
554
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
555
     *
556
     * @param string $property
557
     *
558
     * @return string|false method name if it exists
559
     */
560 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...
561
    {
562
        $class = get_called_class();
563
564
        $k = $class.':'.$property;
565
        if (!array_key_exists($k, self::$accessors)) {
566
            $inflector = Inflector::get();
567
            $method = 'get'.$inflector->camelize($property).'Value';
568
569
            if (!method_exists($class, $method)) {
570
                $method = false;
571
            }
572
573
            self::$accessors[$k] = $method;
574
        }
575
576
        return self::$accessors[$k];
577
    }
578
579
    /**
580
     * Checks if a given property is a relationship.
581
     *
582
     * @param string $property
583
     *
584
     * @return bool
585
     */
586
    public static function isRelationship($property)
587
    {
588
        return in_array($property, static::$relationships);
589
    }
590
591
    /**
592
     * Gets the string date format for a property. Defaults to
593
     * UNIX timestamps.
594
     *
595
     * @param string $property
596
     *
597
     * @return string
598
     */
599
    public static function getDateFormat($property)
600
    {
601
        if (isset(static::$dates[$property])) {
602
            return static::$dates[$property];
603
        }
604
605
        return self::DEFAULT_DATE_FORMAT;
606
    }
607
608
    /**
609
     * Gets the title of a property.
610
     *
611
     * @param string $name
612
     *
613
     * @return string
614
     */
615
    public static function getPropertyTitle($name)
616
    {
617
        // attmept to fetch the title from the Locale service
618
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
619
        if (self::$locale && $title = self::$locale->t($k)) {
620
            if ($title != $k) {
621
                return $title;
622
            }
623
        }
624
625
        return Inflector::get()->humanize($name);
626
    }
627
628
    /**
629
     * Gets the type cast for a property.
630
     *
631
     * @param string $property
632
     *
633
     * @return string|null
634
     */
635
    public static function getPropertyType($property)
636
    {
637
        if (property_exists(get_called_class(), 'casts')) {
638
            return array_value(static::$casts, $property);
639
        }
640
    }
641
642
    /**
643
     * Casts a value to a given type.
644
     *
645
     * @param string|null $type
646
     * @param mixed       $value
647
     * @param string      $property optional property name
648
     *
649
     * @return mixed casted value
650
     */
651
    public static function cast($type, $value, $property = null)
652
    {
653
        if ($value === null) {
654
            return;
655
        }
656
657
        if ($type == self::TYPE_DATE) {
658
            $format = self::getDateFormat($property);
659
660
            return Property::to_date($value, $format);
661
        }
662
663
        $m = 'to_'.$type;
664
665
        return Property::$m($value);
666
    }
667
668
    /**
669
     * Gets the properties of this model.
670
     *
671
     * @return array
672
     */
673
    public function getProperties()
674
    {
675
        return array_unique(array_merge(
676
            static::$ids, array_keys($this->_values)));
677
    }
678
679
    /**
680
     * Checks if the model has a property.
681
     *
682
     * @param string $property
683
     *
684
     * @return bool has property
685
     */
686
    public function hasProperty($property)
687
    {
688
        return array_key_exists($property, $this->_values) ||
689
               in_array($property, static::$ids);
690
    }
691
692
    /////////////////////////////
693
    // Values
694
    /////////////////////////////
695
696
    /**
697
     * Sets an unsaved value.
698
     *
699
     * @param string $name
700
     * @param mixed  $value
701
     * @param bool   $unsaved when true, sets an unsaved value
702
     *
703
     * @throws BadMethodCallException when setting a relationship
704
     *
705
     * @return self
706
     */
707
    public function setValue($name, $value, $unsaved = true)
708
    {
709
        if (static::isRelationship($name)) {
710
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
711
        }
712
713
        // cast the value
714
        if ($type = static::getPropertyType($name)) {
715
            $value = static::cast($type, $value, $name);
716
        }
717
718
        // apply any mutators
719
        if ($mutator = self::getMutator($name)) {
720
            $value = $this->$mutator($value);
721
        }
722
723
        // save the value on the model property
724
        if ($unsaved) {
725
            $this->_unsaved[$name] = $value;
726
        } else {
727
            $this->_values[$name] = $value;
728
        }
729
730
        // if changing property, remove relation model
731
        // DEPRECATED
732
        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...
733
            unset($this->_relationships[$name]);
734
        }
735
736
        return $this;
737
    }
738
739
    /**
740
     * Sets a collection values on the model from an untrusted
741
     * input. Also known as mass assignment.
742
     *
743
     * @param array $values
744
     *
745
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
746
     *
747
     * @return self
748
     */
749
    public function setValues($values)
750
    {
751
        // check if the model has a mass assignment whitelist
752
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
753
754
        // if no whitelist, then check for a blacklist
755
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
756
757
        foreach ($values as $k => $value) {
758
            // check for mass assignment violations
759
            if (($permitted && !in_array($k, $permitted)) ||
760
                ($protected && in_array($k, $protected))) {
761
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
762
            }
763
764
            $this->setValue($k, $value);
765
        }
766
767
        return $this;
768
    }
769
770
    /**
771
     * Ignores unsaved values when fetching the next value.
772
     *
773
     * @return self
774
     */
775
    public function ignoreUnsaved()
776
    {
777
        $this->_ignoreUnsaved = true;
778
779
        return $this;
780
    }
781
782
    /**
783
     * Gets a list of property values from the model.
784
     *
785
     * @param array $properties list of property values to fetch
786
     *
787
     * @return array
788
     */
789
    public function getValues(array $properties)
790
    {
791
        $result = [];
792
        foreach ($properties as $k) {
793
            $result[$k] = $this->getValue($k);
794
        }
795
796
        $this->_ignoreUnsaved = false;
797
798
        return $result;
799
    }
800
801
    /**
802
     * @deprecated
803
     */
804
    public function get(array $properties)
805
    {
806
        return $this->getValues($properties);
807
    }
808
809
    /**
810
     * Gets a property value from the model.
811
     *
812
     * Values are looked up in this order:
813
     *  1. unsaved values
814
     *  2. local values
815
     *  3. relationships
816
     *
817
     * @throws InvalidArgumentException when a property was requested not present in the values
818
     *
819
     * @return mixed
820
     */
821
    private function getValue($property)
822
    {
823
        $value = null;
824
        $accessor = self::getAccessor($property);
825
826
        // first check for unsaved values
827
        if (!$this->_ignoreUnsaved && array_key_exists($property, $this->_unsaved)) {
828
            $value = $this->_unsaved[$property];
829
830
        // then check the normal value store
831
        } elseif (array_key_exists($property, $this->_values)) {
832
            $value = $this->_values[$property];
833
834
        // get relationship values
835
        } elseif (static::isRelationship($property)) {
836
            $value = $this->loadRelationship($property);
837
838
        // throw an exception for non-properties
839
        // that do not have an accessor
840
        } elseif ($accessor === false && !in_array($property, static::$ids)) {
841
            throw new InvalidArgumentException(static::modelName().' does not have a `'.$property.'` property.');
842
        }
843
844
        // call any accessors
845
        if ($accessor !== false) {
846
            return $this->$accessor($value);
847
        }
848
849
        return $value;
850
    }
851
852
    /**
853
     * Converts the model to an array.
854
     *
855
     * @return array model array
856
     */
857
    public function toArray()
858
    {
859
        // build the list of properties to retrieve
860
        $properties = $this->getProperties();
861
862
        // remove any hidden properties
863
        if (property_exists($this, 'hidden')) {
864
            $properties = array_diff($properties, static::$hidden);
865
        }
866
867
        // include any appended properties
868
        if (property_exists($this, 'appended')) {
869
            $properties = array_unique(array_merge($properties, static::$appended));
870
        }
871
872
        // get the values for the properties
873
        $result = $this->getValues($properties);
874
875
        foreach ($result as $k => &$value) {
876
            // convert any models to arrays
877
            if ($value instanceof self) {
878
                $value = $value->toArray();
879
            // convert any Carbon objects to date strings
880
            } elseif ($value instanceof Carbon) {
881
                $format = self::getDateFormat($k);
882
                $value = $value->format($format);
883
            }
884
        }
885
886
        return $result;
887
    }
888
889
    /////////////////////////////
890
    // Persistence
891
    /////////////////////////////
892
893
    /**
894
     * Saves the model.
895
     *
896
     * @return bool
897
     */
898
    public function save()
899
    {
900
        if (!$this->_persisted) {
901
            return $this->create();
902
        }
903
904
        return $this->set();
905
    }
906
907
    /**
908
     * Creates a new model.
909
     *
910
     * @param array $data optional key-value properties to set
911
     *
912
     * @return bool
913
     *
914
     * @throws BadMethodCallException when called on an existing model
915
     */
916
    public function create(array $data = [])
917
    {
918
        if ($this->_persisted) {
919
            throw new BadMethodCallException('Cannot call create() on an existing model');
920
        }
921
922
        // mass assign values passed into create()
923
        $this->setValues($data);
924
925
        // add in any preset values
926
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
927
928
        // dispatch the model.creating event
929
        if (!$this->dispatch(ModelEvent::CREATING)) {
930
            return false;
931
        }
932
933
        // validate the model
934
        if (!$this->valid()) {
935
            return false;
936
        }
937
938
        // persist the model in the data layer
939
        if (!self::getAdapter()->createModel($this, $this->_unsaved)) {
940
            return false;
941
        }
942
943
        // update the model with the persisted values and new ID(s)
944
        $newValues = array_replace(
945
            $this->_unsaved,
946
            $this->getNewIds());
947
        $this->refreshWith($newValues);
948
949
        // dispatch the model.created event
950
        return $this->dispatch(ModelEvent::CREATED);
951
    }
952
953
    /**
954
     * Gets the IDs for a newly created model.
955
     *
956
     * @return string
957
     */
958
    protected function getNewIds()
959
    {
960
        $ids = [];
961
        foreach (static::$ids as $k) {
962
            // check if the ID property was already given,
963
            if (isset($this->_unsaved[$k])) {
964
                $ids[$k] = $this->_unsaved[$k];
965
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
966
            } else {
967
                $ids[$k] = self::getAdapter()->getCreatedID($this, $k);
968
            }
969
        }
970
971
        return $ids;
972
    }
973
974
    /**
975
     * Updates the model.
976
     *
977
     * @param array $data optional key-value properties to set
978
     *
979
     * @return bool
980
     *
981
     * @throws BadMethodCallException when not called on an existing model
982
     */
983
    public function set(array $data = [])
984
    {
985
        if (!$this->_persisted) {
986
            throw new BadMethodCallException('Can only call set() on an existing model');
987
        }
988
989
        // mass assign values passed into set()
990
        $this->setValues($data);
991
992
        // not updating anything?
993
        if (count($this->_unsaved) === 0) {
994
            return true;
995
        }
996
997
        // dispatch the model.updating event
998
        if (!$this->dispatch(ModelEvent::UPDATING)) {
999
            return false;
1000
        }
1001
1002
        // validate the model
1003
        if (!$this->valid()) {
1004
            return false;
1005
        }
1006
1007
        // persist the model in the data layer
1008
        if (!self::getAdapter()->updateModel($this, $this->_unsaved)) {
1009
            return false;
1010
        }
1011
1012
        // update the model with the persisted values
1013
        $this->refreshWith($this->_unsaved);
1014
1015
        // dispatch the model.updated event
1016
        return $this->dispatch(ModelEvent::UPDATED);
1017
    }
1018
1019
    /**
1020
     * Delete the model.
1021
     *
1022
     * @return bool success
1023
     */
1024
    public function delete()
1025
    {
1026
        if (!$this->_persisted) {
1027
            throw new BadMethodCallException('Can only call delete() on an existing model');
1028
        }
1029
1030
        // dispatch the model.deleting event
1031
        if (!$this->dispatch(ModelEvent::DELETING)) {
1032
            return false;
1033
        }
1034
1035
        // delete the model in the data layer
1036
        if (!self::getAdapter()->deleteModel($this)) {
1037
            return false;
1038
        }
1039
1040
        // dispatch the model.deleted event
1041
        if (!$this->dispatch(ModelEvent::DELETED)) {
1042
            return false;
1043
        }
1044
1045
        $this->_persisted = false;
1046
1047
        return true;
1048
    }
1049
1050
    /**
1051
     * Tells if the model has been persisted.
1052
     *
1053
     * @return bool
1054
     */
1055
    public function persisted()
1056
    {
1057
        return $this->_persisted;
1058
    }
1059
1060
    /**
1061
     * Loads the model from the data layer.
1062
     *
1063
     * @return self
1064
     *
1065
     * @throws NotFoundException
1066
     */
1067
    public function refresh()
1068
    {
1069
        if (!$this->_persisted) {
1070
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
1071
        }
1072
1073
        $query = static::query();
1074
        $query->where($this->ids());
1075
1076
        $values = self::getAdapter()->queryModels($query);
1077
1078
        if (count($values) === 0) {
1079
            return $this;
1080
        }
1081
1082
        // clear any relations
1083
        // DEPRECATED
1084
        $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...
1085
1086
        return $this->refreshWith($values[0]);
1087
    }
1088
1089
    /**
1090
     * Loads values into the model retrieved from the data layer.
1091
     *
1092
     * @param array $values values
1093
     *
1094
     * @return self
1095
     */
1096
    public function refreshWith(array $values)
1097
    {
1098
        // cast the values
1099
        if (property_exists($this, 'casts')) {
1100
            foreach ($values as $k => &$value) {
1101
                if ($type = static::getPropertyType($k)) {
1102
                    $value = static::cast($type, $value, $k);
1103
                }
1104
            }
1105
        }
1106
1107
        $this->_persisted = true;
1108
        $this->_values = $values;
1109
        $this->_unsaved = [];
1110
1111
        return $this;
1112
    }
1113
1114
    /////////////////////////////
1115
    // Queries
1116
    /////////////////////////////
1117
1118
    /**
1119
     * Generates a new query instance.
1120
     *
1121
     * @return Query
1122
     */
1123
    public static function query()
1124
    {
1125
        // Create a new model instance for the query to ensure
1126
        // that the model's initialize() method gets called.
1127
        // Otherwise, the property definitions will be incomplete.
1128
        $model = new static();
1129
1130
        return new Query($model);
1131
    }
1132
1133
    /**
1134
     * Finds a single instance of a model given it's ID.
1135
     *
1136
     * @param mixed $id
1137
     *
1138
     * @return Model|null
1139
     */
1140
    public static function find($id)
1141
    {
1142
        $model = static::buildFromId($id);
1143
1144
        return static::query()->where($model->ids())->first();
1145
    }
1146
1147
    /**
1148
     * Finds a single instance of a model given it's ID or throws an exception.
1149
     *
1150
     * @param mixed $id
1151
     *
1152
     * @return Model|false
1153
     *
1154
     * @throws NotFoundException when a model could not be found
1155
     */
1156
    public static function findOrFail($id)
1157
    {
1158
        $model = static::find($id);
1159
        if (!$model) {
1160
            throw new NotFoundException('Could not find the requested '.static::modelName());
1161
        }
1162
1163
        return $model;
1164
    }
1165
1166
    /**
1167
     * Gets the toal number of records matching an optional criteria.
1168
     *
1169
     * @param array $where criteria
1170
     *
1171
     * @return int total
1172
     */
1173
    public static function totalRecords(array $where = [])
1174
    {
1175
        $query = static::query();
1176
        $query->where($where);
1177
1178
        return self::getAdapter()->totalRecords($query);
1179
    }
1180
1181
    /////////////////////////////
1182
    // Relationships
1183
    /////////////////////////////
1184
1185
    /**
1186
     * Creates the parent side of a One-To-One relationship.
1187
     *
1188
     * @param string $model      foreign model class
1189
     * @param string $foreignKey identifying key on foreign model
1190
     * @param string $localKey   identifying key on local model
1191
     *
1192
     * @return \Pulsar\Relation\Relation
1193
     */
1194
    public function hasOne($model, $foreignKey = '', $localKey = '')
1195
    {
1196
        return new HasOne($this, $localKey, $model, $foreignKey);
1197
    }
1198
1199
    /**
1200
     * Creates the child side of a One-To-One or One-To-Many relationship.
1201
     *
1202
     * @param string $model      foreign model class
1203
     * @param string $foreignKey identifying key on foreign model
1204
     * @param string $localKey   identifying key on local model
1205
     *
1206
     * @return \Pulsar\Relation\Relation
1207
     */
1208
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1209
    {
1210
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1211
    }
1212
1213
    /**
1214
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1215
     *
1216
     * @param string $model      foreign model class
1217
     * @param string $foreignKey identifying key on foreign model
1218
     * @param string $localKey   identifying key on local model
1219
     *
1220
     * @return \Pulsar\Relation\Relation
1221
     */
1222
    public function hasMany($model, $foreignKey = '', $localKey = '')
1223
    {
1224
        return new HasMany($this, $localKey, $model, $foreignKey);
1225
    }
1226
1227
    /**
1228
     * Creates the child side of a Many-To-Many relationship.
1229
     *
1230
     * @param string $model      foreign model class
1231
     * @param string $tablename  pivot table name
1232
     * @param string $foreignKey identifying key on foreign model
1233
     * @param string $localKey   identifying key on local model
1234
     *
1235
     * @return \Pulsar\Relation\Relation
1236
     */
1237
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1238
    {
1239
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1240
    }
1241
1242
    /**
1243
     * Loads a given relationship (if not already) and
1244
     * returns its results.
1245
     *
1246
     * @param string $name
1247
     *
1248
     * @return mixed
1249
     */
1250
    protected function loadRelationship($name)
1251
    {
1252
        if (!isset($this->_values[$name])) {
1253
            $relationship = $this->$name();
1254
            $this->_values[$name] = $relationship->getResults();
1255
        }
1256
1257
        return $this->_values[$name];
1258
    }
1259
1260
    /**
1261
     * @deprecated
1262
     * Gets a relationship model with a has one relationship
1263
     *
1264
     * @param string $k property
1265
     *
1266
     * @return \Pulsar\Model|null
1267
     */
1268
    public function relation($k)
1269
    {
1270
        if (!isset(static::$relationshipsDeprecated[$k])) {
1271
            return;
1272
        }
1273
1274
        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...
1275
            $model = static::$relationshipsDeprecated[$k];
1276
            $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...
1277
        }
1278
1279
        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...
1280
    }
1281
1282
    /////////////////////////////
1283
    // Events
1284
    /////////////////////////////
1285
1286
    /**
1287
     * Gets the event dispatcher.
1288
     *
1289
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1290
     */
1291
    public static function getDispatcher($ignoreCache = false)
1292
    {
1293
        $class = get_called_class();
1294
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1295
            self::$dispatchers[$class] = new EventDispatcher();
1296
        }
1297
1298
        return self::$dispatchers[$class];
1299
    }
1300
1301
    /**
1302
     * Subscribes to a listener to an event.
1303
     *
1304
     * @param string   $event    event name
1305
     * @param callable $listener
1306
     * @param int      $priority optional priority, higher #s get called first
1307
     */
1308
    public static function listen($event, callable $listener, $priority = 0)
1309
    {
1310
        static::getDispatcher()->addListener($event, $listener, $priority);
1311
    }
1312
1313
    /**
1314
     * Adds a listener to the model.creating event.
1315
     *
1316
     * @param callable $listener
1317
     * @param int      $priority
1318
     */
1319
    public static function creating(callable $listener, $priority = 0)
1320
    {
1321
        static::listen(ModelEvent::CREATING, $listener, $priority);
1322
    }
1323
1324
    /**
1325
     * Adds a listener to the model.created event.
1326
     *
1327
     * @param callable $listener
1328
     * @param int      $priority
1329
     */
1330
    public static function created(callable $listener, $priority = 0)
1331
    {
1332
        static::listen(ModelEvent::CREATED, $listener, $priority);
1333
    }
1334
1335
    /**
1336
     * Adds a listener to the model.updating event.
1337
     *
1338
     * @param callable $listener
1339
     * @param int      $priority
1340
     */
1341
    public static function updating(callable $listener, $priority = 0)
1342
    {
1343
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1344
    }
1345
1346
    /**
1347
     * Adds a listener to the model.updated event.
1348
     *
1349
     * @param callable $listener
1350
     * @param int      $priority
1351
     */
1352
    public static function updated(callable $listener, $priority = 0)
1353
    {
1354
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1355
    }
1356
1357
    /**
1358
     * Adds a listener to the model.creating and model.updating events.
1359
     *
1360
     * @param callable $listener
1361
     * @param int      $priority
1362
     */
1363
    public static function saving(callable $listener, $priority = 0)
1364
    {
1365
        static::listen(ModelEvent::CREATING, $listener, $priority);
1366
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1367
    }
1368
1369
    /**
1370
     * Adds a listener to the model.created and model.updated events.
1371
     *
1372
     * @param callable $listener
1373
     * @param int      $priority
1374
     */
1375
    public static function saved(callable $listener, $priority = 0)
1376
    {
1377
        static::listen(ModelEvent::CREATED, $listener, $priority);
1378
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1379
    }
1380
1381
    /**
1382
     * Adds a listener to the model.deleting event.
1383
     *
1384
     * @param callable $listener
1385
     * @param int      $priority
1386
     */
1387
    public static function deleting(callable $listener, $priority = 0)
1388
    {
1389
        static::listen(ModelEvent::DELETING, $listener, $priority);
1390
    }
1391
1392
    /**
1393
     * Adds a listener to the model.deleted event.
1394
     *
1395
     * @param callable $listener
1396
     * @param int      $priority
1397
     */
1398
    public static function deleted(callable $listener, $priority = 0)
1399
    {
1400
        static::listen(ModelEvent::DELETED, $listener, $priority);
1401
    }
1402
1403
    /**
1404
     * Dispatches an event.
1405
     *
1406
     * @param string $eventName
1407
     *
1408
     * @return bool true when the event propagated fully without being stopped
1409
     */
1410
    protected function dispatch($eventName)
1411
    {
1412
        $event = new ModelEvent($this);
1413
1414
        static::getDispatcher()->dispatch($eventName, $event);
1415
1416
        return !$event->isPropagationStopped();
1417
    }
1418
1419
    /////////////////////////////
1420
    // Validation
1421
    /////////////////////////////
1422
1423
    /**
1424
     * Gets the error stack for this model instance. Used to
1425
     * keep track of validation errors.
1426
     *
1427
     * @return Errors
1428
     */
1429
    public function errors()
1430
    {
1431
        if (!$this->_errors) {
1432
            $this->_errors = new Errors($this, self::$locale);
1433
        }
1434
1435
        return $this->_errors;
1436
    }
1437
1438
    /**
1439
     * Checks if the model is valid in its current state.
1440
     *
1441
     * @return bool
1442
     */
1443
    public function valid()
1444
    {
1445
        // clear any previous errors
1446
        $this->errors()->clear();
1447
1448
        // run the validator against the model values
1449
        $validator = $this->getValidator();
1450
        $values = $this->_unsaved + $this->_values;
1451
        $validated = $validator->validate($values);
1452
1453
        // add back any modified unsaved values
1454
        foreach (array_keys($this->_unsaved) as $k) {
1455
            $this->_unsaved[$k] = $values[$k];
1456
        }
1457
1458
        return $validated;
1459
    }
1460
1461
    /**
1462
     * Gets a new validator instance for this model.
1463
     *
1464
     * @return Validator
1465
     */
1466
    public function getValidator()
1467
    {
1468
        return new Validator(static::$validations, $this->errors());
1469
    }
1470
}
1471