Completed
Push — master ( c395d1...1934a0 )
by Jared
02:05
created

Model::ids()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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';
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
            $values = array_replace(
166
                $this->defaultValuesDeprecated(),
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Model::defaultValuesDeprecated() 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...
167
                $values);
168
        }
169
170
        foreach ($values as $k => $v) {
171
            $this->setValue($k, $v, false);
172
        }
173
174
        // ensure the initialize function is called only once
175
        $k = get_called_class();
176
        if (!isset(self::$initialized[$k])) {
177
            $this->initialize();
178
            self::$initialized[$k] = true;
179
        }
180
    }
181
182
    /**
183
     * @deprecated
184
     * Sets the default values from a deprecated $properties format
185
     *
186
     * @return array
187
     */
188
    private function defaultValuesDeprecated()
189
    {
190
        $values = [];
191
        foreach (static::$properties as $k => $definition) {
192
            $values[$k] = array_value($definition, 'default');
193
        }
194
195
        return $values;
196
    }
197
198
    /**
199
     * The initialize() method is called once per model. It's used
200
     * to perform any one-off tasks before the model gets
201
     * constructed. This is a great place to add any model
202
     * properties. When extending this method be sure to call
203
     * parent::initialize() as some important stuff happens here.
204
     * If extending this method to add properties then you should
205
     * call parent::initialize() after adding any properties.
206
     */
207
    protected function initialize()
208
    {
209
        // parse deprecated property definitions
210
        if (property_exists($this, 'properties')) {
211
            $this->initializeDeprecated();
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Model::initializeDeprecated() 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...
212
        }
213
214
        // add in the default ID property
215
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
216
            if (property_exists($this, 'casts') && !isset(static::$casts[self::DEFAULT_ID_PROPERTY])) {
217
                static::$casts[self::DEFAULT_ID_PROPERTY] = self::TYPE_INTEGER;
218
            }
219
        }
220
221
        // generates created_at and updated_at timestamps
222
        if (property_exists($this, 'autoTimestamps')) {
223
            $this->installAutoTimestamps();
224
        }
225
    }
226
227
    /**
228
     * @deprecated
229
     * Parses a deprecated $properties format
230
     *
231
     * @return self
232
     */
233
    private function initializeDeprecated()
234
    {
235
        foreach (static::$properties as $k => $definition) {
236
            // parse property types
237
            if (isset($definition['type'])) {
238
                static::$casts[$k] = $definition['type'];
239
            }
240
241
            // parse validations
242
            $rules = [];
243
            if (isset($definition['null'])) {
244
                $rules[] = ['skip_empty', []];
245
            }
246
247
            if (isset($definition['required'])) {
248
                $rules[] = ['required', []];
249
            }
250
251
            if (isset($definition['validate'])) {
252
                if (is_callable($definition['validate'])) {
253
                    $rules[] = ['custom', [$definition['validate']]];
254
                } else {
255
                    // explodes the string into a a list of strings
256
                    // containing rules and parameters
257
                    $pieces = explode('|', $definition['validate']);
258 View Code Duplication
                    foreach ($pieces as $piece) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
259
                        $exp = explode(':', $piece);
260
                        // [0] = rule method
261
                        $method = $exp[0];
262
                        // [1] = optional method parameters
263
                        $parameters = isset($exp[1]) ? explode(',', $exp[1]) : [];
264
265
                        $rules[] = [$method, $parameters];
266
                    }
267
                }
268
            }
269
270
            if (isset($definition['unique'])) {
271
                $rules[] = ['unique', []];
272
            }
273
274
            if ($rules) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rules 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...
275
                static::$validations[$k] = $rules;
276
            }
277
278
            // parse date formats
279
            if (property_exists($this, 'autoTimestamps')) {
280
                static::$dates['created_at'] = 'Y-m-d H:i:s';
281
                static::$dates['updated_at'] = 'Y-m-d H:i:s';
282
            }
283
284
            // parse deprecated relationships
285
            if (isset($definition['relation'])) {
286
                static::$relationshipsDeprecated[$k] = $definition['relation'];
287
            }
288
289
            // parse protected properties
290
            if (isset($definition['mutable']) && in_array($definition['mutable'], [self::IMMUTABLE, self::MUTABLE_CREATE_ONLY])) {
291
                static::$protected[] = $k;
292
            }
293
        }
294
295
        return $this;
296
    }
297
298
    /**
299
     * Installs the automatic timestamp properties,
300
     * `created_at` and `updated_at`.
301
     */
302
    private function installAutoTimestamps()
303
    {
304
        if (property_exists($this, 'casts')) {
305
            static::$casts['created_at'] = self::TYPE_DATE;
306
            static::$casts['updated_at'] = self::TYPE_DATE;
307
        }
308
309
        self::creating(function (ModelEvent $event) {
310
            $model = $event->getModel();
311
            $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...
312
            $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...
313
        });
314
315
        self::updating(function (ModelEvent $event) {
316
            $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...
317
        });
318
    }
319
320
    /**
321
     * Sets the adapter for all models.
322
     *
323
     * @param AdapterInterface $adapter
324
     */
325
    public static function setAdapter(AdapterInterface $adapter)
326
    {
327
        self::$adapter = $adapter;
328
    }
329
330
    /**
331
     * Gets the adapter for all models.
332
     *
333
     * @return AdapterInterface
334
     *
335
     * @throws AdapterMissingException
336
     */
337
    public static function getAdapter()
338
    {
339
        if (!self::$adapter) {
340
            throw new AdapterMissingException('A model adapter has not been set yet.');
341
        }
342
343
        return self::$adapter;
344
    }
345
346
    /**
347
     * Clears the adapter for all models.
348
     */
349
    public static function clearAdapter()
350
    {
351
        self::$adapter = null;
352
    }
353
354
    /**
355
     * @deprecated
356
     */
357
    public static function setDriver(AdapterInterface $adapter)
358
    {
359
        self::$adapter = $adapter;
360
    }
361
362
    /**
363
     * @deprecated
364
     */
365
    public static function getDriver()
366
    {
367
        if (!self::$adapter) {
368
            throw new AdapterMissingException('A model adapter has not been set yet.');
369
        }
370
371
        return self::$adapter;
372
    }
373
374
    /**
375
     * @deprecated
376
     */
377
    public static function clearDriver()
378
    {
379
        self::$adapter = null;
380
    }
381
382
    /**
383
     * Sets the locale instance for all models.
384
     *
385
     * @param Locale $locale
386
     */
387
    public static function setLocale(Locale $locale)
388
    {
389
        self::$locale = $locale;
390
    }
391
392
    /**
393
     * Gets the locale instance for all models.
394
     *
395
     * @return Locale
396
     */
397
    public static function getLocale()
398
    {
399
        return self::$locale;
400
    }
401
402
    /**
403
     * Clears the locale for all models.
404
     */
405
    public static function clearLocale()
406
    {
407
        self::$locale = null;
408
    }
409
410
    /**
411
     * Gets the name of the model without namespacing.
412
     *
413
     * @return string
414
     */
415
    public static function modelName()
416
    {
417
        $namespace = explode('\\', get_called_class());
418
419
        return end($namespace);
420
    }
421
422
    /**
423
     * Gets the table name of the model.
424
     *
425
     * @return string
426
     */
427
    public function getTablename()
428
    {
429
        $name = static::modelName();
430
        if (!isset(self::$tablenames[$name])) {
431
            $inflector = Inflector::get();
432
433
            self::$tablenames[$name] = $inflector->camelize($inflector->pluralize($name));
434
        }
435
436
        return self::$tablenames[$name];
437
    }
438
439
    /**
440
     * Gets the model ID.
441
     *
442
     * @return string|number|null ID
443
     */
444
    public function id()
445
    {
446
        $ids = $this->ids();
447
448
        // if a single ID then return it
449
        if (count($ids) === 1) {
450
            return reset($ids);
451
        }
452
453
        // if multiple IDs then return a comma-separated list
454
        return implode(',', $ids);
455
    }
456
457
    /**
458
     * Gets a key-value map of the model ID.
459
     *
460
     * @return array ID map
461
     */
462
    public function ids()
463
    {
464
        return $this->getValues(static::$ids);
465
    }
466
467
    /////////////////////////////
468
    // Magic Methods
469
    /////////////////////////////
470
471
    public function __toString()
472
    {
473
        return get_called_class().'('.$this->id().')';
474
    }
475
476
    public function __get($name)
477
    {
478
        $value = $this->getValue($name);
479
        $this->_ignoreUnsaved = false;
480
481
        return $value;
482
    }
483
484
    public function __set($name, $value)
485
    {
486
        $this->setValue($name, $value);
487
    }
488
489
    public function __isset($name)
490
    {
491
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
492
    }
493
494
    public function __unset($name)
495
    {
496
        if (static::isRelationship($name)) {
497
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
498
        }
499
500
        if (array_key_exists($name, $this->_unsaved)) {
501
            unset($this->_unsaved[$name]);
502
        }
503
504
        // if changing property, remove relation model
505
        // DEPRECATED
506
        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...
507
            unset($this->_relationships[$name]);
508
        }
509
    }
510
511
    public static function __callStatic($name, $parameters)
512
    {
513
        // Any calls to unkown static methods should be deferred to
514
        // the query. This allows calls like User::where()
515
        // 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...
516
        return call_user_func_array([static::query(), $name], $parameters);
517
    }
518
519
    /////////////////////////////
520
    // ArrayAccess Interface
521
    /////////////////////////////
522
523
    public function offsetExists($offset)
524
    {
525
        return isset($this->$offset);
526
    }
527
528
    public function offsetGet($offset)
529
    {
530
        return $this->$offset;
531
    }
532
533
    public function offsetSet($offset, $value)
534
    {
535
        $this->$offset = $value;
536
    }
537
538
    public function offsetUnset($offset)
539
    {
540
        unset($this->$offset);
541
    }
542
543
    /////////////////////////////
544
    // Property Definitions
545
    /////////////////////////////
546
547
    /**
548
     * Gets the names of the model ID properties.
549
     *
550
     * @return array
551
     */
552
    public static function getIdProperties()
553
    {
554
        return static::$ids;
555
    }
556
557
    /**
558
     * Builds an existing model instance given a single ID value or
559
     * ordered array of ID values.
560
     *
561
     * @param mixed $id
562
     *
563
     * @return Model
564
     */
565
    public static function buildFromId($id)
566
    {
567
        $ids = [];
568
        $id = (array) $id;
569
        foreach (static::$ids as $j => $k) {
570
            $ids[$k] = $id[$j];
571
        }
572
573
        $model = new static($ids);
574
575
        return $model;
576
    }
577
578
    /**
579
     * Gets the mutator method name for a given proeprty name.
580
     * Looks for methods in the form of `setPropertyValue`.
581
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
582
     *
583
     * @param string $property
584
     *
585
     * @return string|false method name if it exists
586
     */
587 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...
588
    {
589
        $class = get_called_class();
590
591
        $k = $class.':'.$property;
592
        if (!array_key_exists($k, self::$mutators)) {
593
            $inflector = Inflector::get();
594
            $method = 'set'.$inflector->camelize($property).'Value';
595
596
            if (!method_exists($class, $method)) {
597
                $method = false;
598
            }
599
600
            self::$mutators[$k] = $method;
601
        }
602
603
        return self::$mutators[$k];
604
    }
605
606
    /**
607
     * Gets the accessor method name for a given proeprty name.
608
     * Looks for methods in the form of `getPropertyValue`.
609
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
610
     *
611
     * @param string $property
612
     *
613
     * @return string|false method name if it exists
614
     */
615 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...
616
    {
617
        $class = get_called_class();
618
619
        $k = $class.':'.$property;
620
        if (!array_key_exists($k, self::$accessors)) {
621
            $inflector = Inflector::get();
622
            $method = 'get'.$inflector->camelize($property).'Value';
623
624
            if (!method_exists($class, $method)) {
625
                $method = false;
626
            }
627
628
            self::$accessors[$k] = $method;
629
        }
630
631
        return self::$accessors[$k];
632
    }
633
634
    /**
635
     * Checks if a given property is a relationship.
636
     *
637
     * @param string $property
638
     *
639
     * @return bool
640
     */
641
    public static function isRelationship($property)
642
    {
643
        return in_array($property, static::$relationships);
644
    }
645
646
    /**
647
     * Gets the string date format for a property. Defaults to
648
     * UNIX timestamps.
649
     *
650
     * @param string $property
651
     *
652
     * @return string
653
     */
654
    public static function getDateFormat($property)
655
    {
656
        if (isset(static::$dates[$property])) {
657
            return static::$dates[$property];
658
        }
659
660
        return self::DEFAULT_DATE_FORMAT;
661
    }
662
663
    /**
664
     * Gets the title of a property.
665
     *
666
     * @param string $name
667
     *
668
     * @return string
669
     */
670
    public static function getPropertyTitle($name)
671
    {
672
        // attmept to fetch the title from the Locale service
673
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
674
        if (self::$locale && $title = self::$locale->t($k)) {
675
            if ($title != $k) {
676
                return $title;
677
            }
678
        }
679
680
        return Inflector::get()->humanize($name);
681
    }
682
683
    /**
684
     * Gets the type cast for a property.
685
     *
686
     * @param string $property
687
     *
688
     * @return string|null
689
     */
690
    public static function getPropertyType($property)
691
    {
692
        if (property_exists(get_called_class(), 'casts')) {
693
            return array_value(static::$casts, $property);
694
        }
695
    }
696
697
    /**
698
     * Casts a value to a given type.
699
     *
700
     * @param string|null $type
701
     * @param mixed       $value
702
     * @param string      $property optional property name
703
     *
704
     * @return mixed casted value
705
     */
706
    public static function cast($type, $value, $property = null)
707
    {
708
        if ($value === null) {
709
            return;
710
        }
711
712
        if ($type == self::TYPE_DATE) {
713
            $format = self::getDateFormat($property);
714
715
            return Property::to_date($value, $format);
716
        }
717
718
        $m = 'to_'.$type;
719
720
        return Property::$m($value);
721
    }
722
723
    /**
724
     * Gets the properties of this model.
725
     *
726
     * @return array
727
     */
728
    public function getProperties()
729
    {
730
        return array_unique(array_merge(
731
            static::$ids, array_keys($this->_values)));
732
    }
733
734
    /**
735
     * Checks if the model has a property.
736
     *
737
     * @param string $property
738
     *
739
     * @return bool has property
740
     */
741
    public function hasProperty($property)
742
    {
743
        return array_key_exists($property, $this->_values) ||
744
               in_array($property, static::$ids);
745
    }
746
747
    /////////////////////////////
748
    // Values
749
    /////////////////////////////
750
751
    /**
752
     * Sets an unsaved value.
753
     *
754
     * @param string $name
755
     * @param mixed  $value
756
     * @param bool   $unsaved when true, sets an unsaved value
757
     *
758
     * @throws BadMethodCallException when setting a relationship
759
     *
760
     * @return self
761
     */
762
    public function setValue($name, $value, $unsaved = true)
763
    {
764
        if (static::isRelationship($name)) {
765
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
766
        }
767
768
        // cast the value
769
        if ($type = static::getPropertyType($name)) {
770
            $value = static::cast($type, $value, $name);
771
        }
772
773
        // apply any mutators
774
        if ($mutator = self::getMutator($name)) {
775
            $value = $this->$mutator($value);
776
        }
777
778
        // save the value on the model property
779
        if ($unsaved) {
780
            $this->_unsaved[$name] = $value;
781
        } else {
782
            $this->_values[$name] = $value;
783
        }
784
785
        // if changing property, remove relation model
786
        // DEPRECATED
787
        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...
788
            unset($this->_relationships[$name]);
789
        }
790
791
        return $this;
792
    }
793
794
    /**
795
     * Sets a collection values on the model from an untrusted
796
     * input. Also known as mass assignment.
797
     *
798
     * @param array $values
799
     *
800
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
801
     *
802
     * @return self
803
     */
804
    public function setValues($values)
805
    {
806
        // check if the model has a mass assignment whitelist
807
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
808
809
        // if no whitelist, then check for a blacklist
810
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
811
812
        foreach ($values as $k => $value) {
813
            // check for mass assignment violations
814
            if (($permitted && !in_array($k, $permitted)) ||
815
                ($protected && in_array($k, $protected))) {
816
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
817
            }
818
819
            $this->setValue($k, $value);
820
        }
821
822
        return $this;
823
    }
824
825
    /**
826
     * Ignores unsaved values when fetching the next value.
827
     *
828
     * @return self
829
     */
830
    public function ignoreUnsaved()
831
    {
832
        $this->_ignoreUnsaved = true;
833
834
        return $this;
835
    }
836
837
    /**
838
     * Gets a list of property values from the model.
839
     *
840
     * @param array $properties list of property values to fetch
841
     *
842
     * @return array
843
     */
844
    public function getValues(array $properties)
845
    {
846
        $result = [];
847
        foreach ($properties as $k) {
848
            $result[$k] = $this->getValue($k);
849
        }
850
851
        $this->_ignoreUnsaved = false;
852
853
        return $result;
854
    }
855
856
    /**
857
     * @deprecated
858
     */
859
    public function get(array $properties)
860
    {
861
        return $this->getValues($properties);
862
    }
863
864
    /**
865
     * Gets a property value from the model.
866
     *
867
     * Values are looked up in this order:
868
     *  1. unsaved values
869
     *  2. local values
870
     *  3. relationships
871
     *
872
     * @throws InvalidArgumentException when a property was requested not present in the values
873
     *
874
     * @return mixed
875
     */
876
    private function getValue($property)
877
    {
878
        $value = null;
879
        $accessor = self::getAccessor($property);
880
881
        // first check for unsaved values
882
        if (!$this->_ignoreUnsaved && array_key_exists($property, $this->_unsaved)) {
883
            $value = $this->_unsaved[$property];
884
885
        // then check the normal value store
886
        } elseif (array_key_exists($property, $this->_values)) {
887
            $value = $this->_values[$property];
888
889
        // get relationship values
890
        } elseif (static::isRelationship($property)) {
891
            $value = $this->loadRelationship($property);
892
893
        // throw an exception for non-properties
894
        // that do not have an accessor
895
        } elseif ($accessor === false && !in_array($property, static::$ids)) {
896
            throw new InvalidArgumentException(static::modelName().' does not have a `'.$property.'` property.');
897
        }
898
899
        // call any accessors
900
        if ($accessor !== false) {
901
            return $this->$accessor($value);
902
        }
903
904
        return $value;
905
    }
906
907
    /**
908
     * Converts the model to an array.
909
     *
910
     * @return array model array
911
     */
912
    public function toArray()
913
    {
914
        // build the list of properties to retrieve
915
        $properties = $this->getProperties();
916
917
        // remove any hidden properties
918
        if (property_exists($this, 'hidden')) {
919
            $properties = array_diff($properties, static::$hidden);
920
        }
921
922
        // include any appended properties
923
        if (property_exists($this, 'appended')) {
924
            $properties = array_unique(array_merge($properties, static::$appended));
925
        }
926
927
        // get the values for the properties
928
        $result = $this->getValues($properties);
929
930
        foreach ($result as $k => &$value) {
931
            // convert any models to arrays
932
            if ($value instanceof self) {
933
                $value = $value->toArray();
934
            // convert any Carbon objects to date strings
935
            } elseif ($value instanceof Carbon) {
936
                $format = self::getDateFormat($k);
937
                $value = $value->format($format);
938
            }
939
        }
940
941
        return $result;
942
    }
943
944
    /////////////////////////////
945
    // Persistence
946
    /////////////////////////////
947
948
    /**
949
     * Saves the model.
950
     *
951
     * @return bool
952
     */
953
    public function save()
954
    {
955
        if (!$this->_persisted) {
956
            return $this->create();
957
        }
958
959
        return $this->set();
960
    }
961
962
    /**
963
     * Creates a new model.
964
     *
965
     * @param array $data optional key-value properties to set
966
     *
967
     * @return bool
968
     *
969
     * @throws BadMethodCallException when called on an existing model
970
     */
971
    public function create(array $data = [])
972
    {
973
        if ($this->_persisted) {
974
            throw new BadMethodCallException('Cannot call create() on an existing model');
975
        }
976
977
        // mass assign values passed into create()
978
        $this->setValues($data);
979
980
        // add in any preset values
981
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
982
983
        // dispatch the model.creating event
984
        if (!$this->dispatch(ModelEvent::CREATING)) {
985
            return false;
986
        }
987
988
        // validate the model
989
        if (!$this->valid()) {
990
            return false;
991
        }
992
993
        // persist the model in the data layer
994
        if (!self::getAdapter()->createModel($this, $this->_unsaved)) {
995
            return false;
996
        }
997
998
        // update the model with the persisted values and new ID(s)
999
        $newValues = array_replace(
1000
            $this->_unsaved,
1001
            $this->getNewIds());
1002
        $this->refreshWith($newValues);
1003
1004
        // dispatch the model.created event
1005
        return $this->dispatch(ModelEvent::CREATED);
1006
    }
1007
1008
    /**
1009
     * Gets the IDs for a newly created model.
1010
     *
1011
     * @return string
1012
     */
1013
    protected function getNewIds()
1014
    {
1015
        $ids = [];
1016
        foreach (static::$ids as $k) {
1017
            // check if the ID property was already given,
1018
            if (isset($this->_unsaved[$k])) {
1019
                $ids[$k] = $this->_unsaved[$k];
1020
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
1021
            } else {
1022
                $ids[$k] = self::getAdapter()->getCreatedID($this, $k);
1023
            }
1024
        }
1025
1026
        return $ids;
1027
    }
1028
1029
    /**
1030
     * Updates the model.
1031
     *
1032
     * @param array $data optional key-value properties to set
1033
     *
1034
     * @return bool
1035
     *
1036
     * @throws BadMethodCallException when not called on an existing model
1037
     */
1038
    public function set(array $data = [])
1039
    {
1040
        if (!$this->_persisted) {
1041
            throw new BadMethodCallException('Can only call set() on an existing model');
1042
        }
1043
1044
        // mass assign values passed into set()
1045
        $this->setValues($data);
1046
1047
        // not updating anything?
1048
        if (count($this->_unsaved) === 0) {
1049
            return true;
1050
        }
1051
1052
        // dispatch the model.updating event
1053
        if (!$this->dispatch(ModelEvent::UPDATING)) {
1054
            return false;
1055
        }
1056
1057
        // DEPRECATED
1058
        if (method_exists($this, 'preSetHook') && !$this->preSetHook($this->_unsaved)) {
0 ignored issues
show
Bug introduced by
The method preSetHook() does not seem to exist on object<Pulsar\Model>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1059
            return false;
1060
        }
1061
1062
        // validate the model
1063
        if (!$this->valid()) {
1064
            return false;
1065
        }
1066
1067
        // persist the model in the data layer
1068
        if (!self::getAdapter()->updateModel($this, $this->_unsaved)) {
1069
            return false;
1070
        }
1071
1072
        // update the model with the persisted values
1073
        $this->refreshWith($this->_unsaved);
1074
1075
        // dispatch the model.updated event
1076
        return $this->dispatch(ModelEvent::UPDATED);
1077
    }
1078
1079
    /**
1080
     * Delete the model.
1081
     *
1082
     * @return bool success
1083
     */
1084
    public function delete()
1085
    {
1086
        if (!$this->_persisted) {
1087
            throw new BadMethodCallException('Can only call delete() on an existing model');
1088
        }
1089
1090
        // dispatch the model.deleting event
1091
        if (!$this->dispatch(ModelEvent::DELETING)) {
1092
            return false;
1093
        }
1094
1095
        // delete the model in the data layer
1096
        if (!self::getAdapter()->deleteModel($this)) {
1097
            return false;
1098
        }
1099
1100
        // dispatch the model.deleted event
1101
        if (!$this->dispatch(ModelEvent::DELETED)) {
1102
            return false;
1103
        }
1104
1105
        $this->_persisted = false;
1106
1107
        return true;
1108
    }
1109
1110
    /**
1111
     * Tells if the model has been persisted.
1112
     *
1113
     * @return bool
1114
     */
1115
    public function persisted()
1116
    {
1117
        return $this->_persisted;
1118
    }
1119
1120
    /**
1121
     * Loads the model from the data layer.
1122
     *
1123
     * @return self
1124
     *
1125
     * @throws NotFoundException
1126
     */
1127
    public function refresh()
1128
    {
1129
        if (!$this->_persisted) {
1130
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
1131
        }
1132
1133
        $query = static::query();
1134
        $query->where($this->ids());
1135
1136
        $values = self::getAdapter()->queryModels($query);
1137
1138
        if (count($values) === 0) {
1139
            return $this;
1140
        }
1141
1142
        // clear any relations
1143
        // DEPRECATED
1144
        $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...
1145
1146
        return $this->refreshWith($values[0]);
1147
    }
1148
1149
    /**
1150
     * Loads values into the model retrieved from the data layer.
1151
     *
1152
     * @param array $values values
1153
     *
1154
     * @return self
1155
     */
1156
    public function refreshWith(array $values)
1157
    {
1158
        // cast the values
1159
        if (property_exists($this, 'casts')) {
1160
            foreach ($values as $k => &$value) {
1161
                if ($type = static::getPropertyType($k)) {
1162
                    $value = static::cast($type, $value, $k);
1163
                }
1164
            }
1165
        }
1166
1167
        $this->_persisted = true;
1168
        $this->_values = $values;
1169
        $this->_unsaved = [];
1170
1171
        return $this;
1172
    }
1173
1174
    /////////////////////////////
1175
    // Queries
1176
    /////////////////////////////
1177
1178
    /**
1179
     * Generates a new query instance.
1180
     *
1181
     * @return Query
1182
     */
1183
    public static function query()
1184
    {
1185
        // Create a new model instance for the query to ensure
1186
        // that the model's initialize() method gets called.
1187
        // Otherwise, the property definitions will be incomplete.
1188
        $model = new static();
1189
1190
        return new Query($model);
1191
    }
1192
1193
    /**
1194
     * Finds a single instance of a model given it's ID.
1195
     *
1196
     * @param mixed $id
1197
     *
1198
     * @return Model|null
1199
     */
1200
    public static function find($id)
1201
    {
1202
        $model = static::buildFromId($id);
1203
1204
        return static::query()->where($model->ids())->first();
1205
    }
1206
1207
    /**
1208
     * Finds a single instance of a model given it's ID or throws an exception.
1209
     *
1210
     * @param mixed $id
1211
     *
1212
     * @return Model|false
1213
     *
1214
     * @throws NotFoundException when a model could not be found
1215
     */
1216
    public static function findOrFail($id)
1217
    {
1218
        $model = static::find($id);
1219
        if (!$model) {
1220
            throw new NotFoundException('Could not find the requested '.static::modelName());
1221
        }
1222
1223
        return $model;
1224
    }
1225
1226
    /**
1227
     * Gets the toal number of records matching an optional criteria.
1228
     *
1229
     * @param array $where criteria
1230
     *
1231
     * @return int total
1232
     */
1233
    public static function totalRecords(array $where = [])
1234
    {
1235
        $query = static::query();
1236
        $query->where($where);
1237
1238
        return self::getAdapter()->totalRecords($query);
1239
    }
1240
1241
    /**
1242
     * @deprecated
1243
     * Checks if the model exists in the database
1244
     *
1245
     * @return bool
1246
     */
1247
    public function exists()
1248
    {
1249
        return static::totalRecords($this->ids()) == 1;
1250
    }
1251
1252
    /////////////////////////////
1253
    // Relationships
1254
    /////////////////////////////
1255
1256
    /**
1257
     * Creates the parent side of a One-To-One relationship.
1258
     *
1259
     * @param string $model      foreign model class
1260
     * @param string $foreignKey identifying key on foreign model
1261
     * @param string $localKey   identifying key on local model
1262
     *
1263
     * @return \Pulsar\Relation\Relation
1264
     */
1265
    public function hasOne($model, $foreignKey = '', $localKey = '')
1266
    {
1267
        return new HasOne($this, $localKey, $model, $foreignKey);
1268
    }
1269
1270
    /**
1271
     * Creates the child side of a One-To-One or One-To-Many relationship.
1272
     *
1273
     * @param string $model      foreign model class
1274
     * @param string $foreignKey identifying key on foreign model
1275
     * @param string $localKey   identifying key on local model
1276
     *
1277
     * @return \Pulsar\Relation\Relation
1278
     */
1279
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1280
    {
1281
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1282
    }
1283
1284
    /**
1285
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1286
     *
1287
     * @param string $model      foreign model class
1288
     * @param string $foreignKey identifying key on foreign model
1289
     * @param string $localKey   identifying key on local model
1290
     *
1291
     * @return \Pulsar\Relation\Relation
1292
     */
1293
    public function hasMany($model, $foreignKey = '', $localKey = '')
1294
    {
1295
        return new HasMany($this, $localKey, $model, $foreignKey);
1296
    }
1297
1298
    /**
1299
     * Creates the child side of a Many-To-Many relationship.
1300
     *
1301
     * @param string $model      foreign model class
1302
     * @param string $tablename  pivot table name
1303
     * @param string $foreignKey identifying key on foreign model
1304
     * @param string $localKey   identifying key on local model
1305
     *
1306
     * @return \Pulsar\Relation\Relation
1307
     */
1308
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1309
    {
1310
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1311
    }
1312
1313
    /**
1314
     * Loads a given relationship (if not already) and
1315
     * returns its results.
1316
     *
1317
     * @param string $name
1318
     *
1319
     * @return mixed
1320
     */
1321
    protected function loadRelationship($name)
1322
    {
1323
        if (!isset($this->_values[$name])) {
1324
            $relationship = $this->$name();
1325
            $this->_values[$name] = $relationship->getResults();
1326
        }
1327
1328
        return $this->_values[$name];
1329
    }
1330
1331
    /**
1332
     * @deprecated
1333
     * Gets a relationship model with a has one relationship
1334
     *
1335
     * @param string $k property
1336
     *
1337
     * @return \Pulsar\Model|null
1338
     */
1339
    public function relation($k)
1340
    {
1341
        if (!isset(static::$relationshipsDeprecated[$k])) {
1342
            return;
1343
        }
1344
1345
        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...
1346
            $model = static::$relationshipsDeprecated[$k];
1347
            $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...
1348
        }
1349
1350
        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...
1351
    }
1352
1353
    /////////////////////////////
1354
    // Events
1355
    /////////////////////////////
1356
1357
    /**
1358
     * Gets the event dispatcher.
1359
     *
1360
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1361
     */
1362
    public static function getDispatcher($ignoreCache = false)
1363
    {
1364
        $class = get_called_class();
1365
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1366
            self::$dispatchers[$class] = new EventDispatcher();
1367
        }
1368
1369
        return self::$dispatchers[$class];
1370
    }
1371
1372
    /**
1373
     * Subscribes to a listener to an event.
1374
     *
1375
     * @param string   $event    event name
1376
     * @param callable $listener
1377
     * @param int      $priority optional priority, higher #s get called first
1378
     */
1379
    public static function listen($event, callable $listener, $priority = 0)
1380
    {
1381
        static::getDispatcher()->addListener($event, $listener, $priority);
1382
    }
1383
1384
    /**
1385
     * Adds a listener to the model.creating event.
1386
     *
1387
     * @param callable $listener
1388
     * @param int      $priority
1389
     */
1390
    public static function creating(callable $listener, $priority = 0)
1391
    {
1392
        static::listen(ModelEvent::CREATING, $listener, $priority);
1393
    }
1394
1395
    /**
1396
     * Adds a listener to the model.created event.
1397
     *
1398
     * @param callable $listener
1399
     * @param int      $priority
1400
     */
1401
    public static function created(callable $listener, $priority = 0)
1402
    {
1403
        static::listen(ModelEvent::CREATED, $listener, $priority);
1404
    }
1405
1406
    /**
1407
     * Adds a listener to the model.updating event.
1408
     *
1409
     * @param callable $listener
1410
     * @param int      $priority
1411
     */
1412
    public static function updating(callable $listener, $priority = 0)
1413
    {
1414
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1415
    }
1416
1417
    /**
1418
     * Adds a listener to the model.updated event.
1419
     *
1420
     * @param callable $listener
1421
     * @param int      $priority
1422
     */
1423
    public static function updated(callable $listener, $priority = 0)
1424
    {
1425
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1426
    }
1427
1428
    /**
1429
     * Adds a listener to the model.creating and model.updating events.
1430
     *
1431
     * @param callable $listener
1432
     * @param int      $priority
1433
     */
1434
    public static function saving(callable $listener, $priority = 0)
1435
    {
1436
        static::listen(ModelEvent::CREATING, $listener, $priority);
1437
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1438
    }
1439
1440
    /**
1441
     * Adds a listener to the model.created and model.updated events.
1442
     *
1443
     * @param callable $listener
1444
     * @param int      $priority
1445
     */
1446
    public static function saved(callable $listener, $priority = 0)
1447
    {
1448
        static::listen(ModelEvent::CREATED, $listener, $priority);
1449
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1450
    }
1451
1452
    /**
1453
     * Adds a listener to the model.deleting event.
1454
     *
1455
     * @param callable $listener
1456
     * @param int      $priority
1457
     */
1458
    public static function deleting(callable $listener, $priority = 0)
1459
    {
1460
        static::listen(ModelEvent::DELETING, $listener, $priority);
1461
    }
1462
1463
    /**
1464
     * Adds a listener to the model.deleted event.
1465
     *
1466
     * @param callable $listener
1467
     * @param int      $priority
1468
     */
1469
    public static function deleted(callable $listener, $priority = 0)
1470
    {
1471
        static::listen(ModelEvent::DELETED, $listener, $priority);
1472
    }
1473
1474
    /**
1475
     * Dispatches an event.
1476
     *
1477
     * @param string $eventName
1478
     *
1479
     * @return bool true when the event propagated fully without being stopped
1480
     */
1481
    protected function dispatch($eventName)
1482
    {
1483
        $event = new ModelEvent($this);
1484
1485
        static::getDispatcher()->dispatch($eventName, $event);
1486
1487
        return !$event->isPropagationStopped();
1488
    }
1489
1490
    /////////////////////////////
1491
    // Validation
1492
    /////////////////////////////
1493
1494
    /**
1495
     * Gets the error stack for this model instance. Used to
1496
     * keep track of validation errors.
1497
     *
1498
     * @return Errors
1499
     */
1500
    public function errors()
1501
    {
1502
        if (!$this->_errors) {
1503
            $this->_errors = new Errors($this, self::$locale);
1504
        }
1505
1506
        return $this->_errors;
1507
    }
1508
1509
    /**
1510
     * Checks if the model is valid in its current state.
1511
     *
1512
     * @return bool
1513
     */
1514
    public function valid()
1515
    {
1516
        // clear any previous errors
1517
        $this->errors()->clear();
1518
1519
        // run the validator against the model values
1520
        $validator = $this->getValidator();
1521
        $values = $this->_unsaved + $this->_values;
1522
        $validated = $validator->validate($values);
1523
1524
        // add back any modified unsaved values
1525
        foreach (array_keys($this->_unsaved) as $k) {
1526
            $this->_unsaved[$k] = $values[$k];
1527
        }
1528
1529
        return $validated;
1530
    }
1531
1532
    /**
1533
     * Gets a new validator instance for this model.
1534
     *
1535
     * @return Validator
1536
     */
1537
    public function getValidator()
1538
    {
1539
        return new Validator(static::$validations, $this->errors());
1540
    }
1541
}
1542