Completed
Push — master ( 66b713...101c92 )
by Jared
57:08
created

Model::getPropertyType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
namespace Pulsar;
12
13
use BadMethodCallException;
14
use ICanBoogie\Inflector;
15
use Infuse\Locale;
16
use InvalidArgumentException;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Exception\DriverMissingException;
19
use Pulsar\Exception\MassAssignmentException;
20
use Pulsar\Exception\NotFoundException;
21
use Pulsar\Relation\HasOne;
22
use Pulsar\Relation\BelongsTo;
23
use Pulsar\Relation\HasMany;
24
use Pulsar\Relation\BelongsToMany;
25
use Pimple\Container;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
28
abstract class Model implements \ArrayAccess
29
{
30
    const TYPE_STRING = 'string';
31
    const TYPE_INTEGER = 'integer';
32
    const TYPE_FLOAT = 'float';
33
    const TYPE_BOOLEAN = 'boolean';
34
    const TYPE_DATE = 'date';
35
    const TYPE_OBJECT = 'object';
36
    const TYPE_ARRAY = 'array';
37
38
    const DEFAULT_ID_PROPERTY = 'id';
39
40
    /////////////////////////////
41
    // Model visible variables
42
    /////////////////////////////
43
44
    /**
45
     * List of model ID property names.
46
     *
47
     * @staticvar array
48
     */
49
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
50
51
    /**
52
     * Property definitions expressed as a key-value map with
53
     * property names as the keys.
54
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
55
     *
56
     * @staticvar array
57
     */
58
    protected static $properties = [];
59
60
    /**
61
     * Validation rules expressed as a key-value map with
62
     * property names as the keys.
63
     * i.e. ['name' => 'string:2'].
64
     *
65
     * @staticvar array
66
     */
67
    protected static $validations = [];
68
69
    /**
70
     * @staticvar array
71
     */
72
    protected static $relationships = [];
73
74
    /**
75
     * @staticvar \Pimple\Container
76
     */
77
    protected static $injectedApp;
78
79
    /**
80
     * @staticvar array
81
     */
82
    protected static $dispatchers;
83
84
    /**
85
     * @var \Pimple\Container
86
     */
87
    protected $app;
88
89
    /**
90
     * @var array
91
     */
92
    protected $_values = [];
93
94
    /**
95
     * @var array
96
     */
97
    protected $_unsaved = [];
98
99
    /**
100
     * @var bool
101
     */
102
    protected $_persisted = false;
103
104
    /**
105
     * @var Errors
106
     */
107
    protected $_errors;
108
109
    /////////////////////////////
110
    // Base model variables
111
    /////////////////////////////
112
113
    /**
114
     * @staticvar array
115
     */
116
    private static $defaultIDProperty = [
117
        self::DEFAULT_ID_PROPERTY => self::TYPE_INTEGER,
118
    ];
119
120
    /**
121
     * @staticvar array
122
     */
123
    private static $timestampProperties = [
124
        'created_at' => self::TYPE_DATE,
125
        'updated_at' => self::TYPE_DATE,
126
    ];
127
128
    /**
129
     * @staticvar array
130
     */
131
    private static $timestampValidations = [
132
        'created_at' => 'timestamp|db_timestamp',
133
        'updated_at' => 'timestamp|db_timestamp',
134
    ];
135
136
    /**
137
     * @staticvar array
138
     */
139
    private static $initialized = [];
140
141
    /**
142
     * @staticvar DriverInterface
143
     */
144
    private static $driver;
145
146
    /**
147
     * @staticvar Locale
148
     */
149
    private static $locale;
150
151
    /**
152
     * @staticvar array
153
     */
154
    private static $accessors = [];
155
156
    /**
157
     * @staticvar array
158
     */
159
    private static $mutators = [];
160
161
    /**
162
     * @var bool
163
     */
164
    private $_ignoreUnsaved;
165
166
    /**
167
     * Creates a new model object.
168
     *
169
     * @param array $values values to fill model with
170
     */
171
    public function __construct(array $values = [])
172
    {
173
        $this->_values = $values;
174
        $this->app = self::$injectedApp;
175
176
        // ensure the initialize function is called only once
177
        $k = get_called_class();
178
        if (!isset(self::$initialized[$k])) {
179
            $this->initialize();
180
            self::$initialized[$k] = true;
181
        }
182
    }
183
184
    /**
185
     * The initialize() method is called once per model. It's used
186
     * to perform any one-off tasks before the model gets
187
     * constructed. This is a great place to add any model
188
     * properties. When extending this method be sure to call
189
     * parent::initialize() as some important stuff happens here.
190
     * If extending this method to add properties then you should
191
     * call parent::initialize() after adding any properties.
192
     */
193
    protected function initialize()
194
    {
195
        // add in the default ID property
196
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
197
            static::$properties[] = self::DEFAULT_ID_PROPERTY;
198
199
            if (property_exists($this, 'casts')) {
200
                static::$casts = array_replace(self::$defaultIDProperty, static::$casts);
201
            }
202
        }
203
204
        // add in the auto timestamp properties
205
        if (property_exists($this, 'autoTimestamps')) {
206
            static::$properties[] = 'created_at';
207
            static::$properties[] = 'updated_at';
208
209
            if (property_exists($this, 'casts')) {
210
                static::$casts = array_replace(self::$timestampProperties, static::$casts);
211
            }
212
213
            static::$validations = array_replace(self::$timestampValidations, static::$validations);
214
        }
215
216
        // order the properties array by name for consistency
217
        // since it is constructed in a random order
218
        sort(static::$properties);
219
    }
220
221
    /**
222
     * Injects a DI container.
223
     *
224
     * @param \Pimple\Container $app
225
     */
226
    public static function inject(Container $app)
227
    {
228
        self::$injectedApp = $app;
229
    }
230
231
    /**
232
     * Gets the DI container used for this model.
233
     *
234
     * @return \Pimple\Container
235
     */
236
    public function getApp()
237
    {
238
        return $this->app;
239
    }
240
241
    /**
242
     * Sets the driver for all models.
243
     *
244
     * @param DriverInterface $driver
245
     */
246
    public static function setDriver(DriverInterface $driver)
247
    {
248
        self::$driver = $driver;
249
    }
250
251
    /**
252
     * Gets the driver for all models.
253
     *
254
     * @return DriverInterface
255
     *
256
     * @throws DriverMissingException
257
     */
258
    public static function getDriver()
259
    {
260
        if (!self::$driver) {
261
            throw new DriverMissingException('A model driver has not been set yet.');
262
        }
263
264
        return self::$driver;
265
    }
266
267
    /**
268
     * Clears the driver for all models.
269
     */
270
    public static function clearDriver()
271
    {
272
        self::$driver = null;
273
    }
274
275
    /**
276
     * Sets the locale instance for all models.
277
     *
278
     * @param Locale $locale
279
     */
280
    public static function setLocale(Locale $locale)
281
    {
282
        self::$locale = $locale;
283
    }
284
285
    /**
286
     * Clears the locale for all models.
287
     */
288
    public static function clearLocale()
289
    {
290
        self::$locale = null;
291
    }
292
293
    /**
294
     * Gets the name of the model without namespacing.
295
     *
296
     * @return string
297
     */
298
    public static function modelName()
299
    {
300
        return explode('\\', get_called_class())[0];
301
    }
302
303
    /**
304
     * Gets the model ID.
305
     *
306
     * @return string|number|null ID
307
     */
308
    public function id()
309
    {
310
        $ids = $this->ids();
311
312
        // if a single ID then return it
313
        if (count($ids) === 1) {
314
            return reset($ids);
315
        }
316
317
        // if multiple IDs then return a comma-separated list
318
        return implode(',', $ids);
319
    }
320
321
    /**
322
     * Gets a key-value map of the model ID.
323
     *
324
     * @return array ID map
325
     */
326
    public function ids()
327
    {
328
        return $this->get(static::$ids);
329
    }
330
331
    /////////////////////////////
332
    // Magic Methods
333
    /////////////////////////////
334
335
    public function __toString()
336
    {
337
        return get_called_class().'('.$this->id().')';
338
    }
339
340
    public function __get($name)
341
    {
342
        return array_values($this->get([$name]))[0];
343
    }
344
345
    public function __set($name, $value)
346
    {
347
        $this->setValue($name, $value);
348
    }
349
350
    public function __isset($name)
351
    {
352
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
353
    }
354
355
    public function __unset($name)
356
    {
357
        if (static::isRelationship($name)) {
358
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
359
        }
360
361
        if (array_key_exists($name, $this->_unsaved)) {
362
            unset($this->_unsaved[$name]);
363
        }
364
    }
365
366
    public static function __callStatic($name, $parameters)
367
    {
368
        // Any calls to unkown static methods should be deferred to
369
        // the query. This allows calls like User::where()
370
        // 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...
371
        return call_user_func_array([static::query(), $name], $parameters);
372
    }
373
374
    /////////////////////////////
375
    // ArrayAccess Interface
376
    /////////////////////////////
377
378
    public function offsetExists($offset)
379
    {
380
        return isset($this->$offset);
381
    }
382
383
    public function offsetGet($offset)
384
    {
385
        return $this->$offset;
386
    }
387
388
    public function offsetSet($offset, $value)
389
    {
390
        $this->$offset = $value;
391
    }
392
393
    public function offsetUnset($offset)
394
    {
395
        unset($this->$offset);
396
    }
397
398
    /////////////////////////////
399
    // Property Definitions
400
    /////////////////////////////
401
402
    /**
403
     * Gets the names of the model ID properties.
404
     *
405
     * @return array
406
     */
407
    public static function getIdProperties()
408
    {
409
        return static::$ids;
410
    }
411
412
    /**
413
     * Builds an existing model instance given a single ID value or
414
     * ordered array of ID values.
415
     *
416
     * @param mixed $id
417
     *
418
     * @return Model
419
     */
420
    public static function buildFromId($id)
421
    {
422
        $ids = [];
423
        $id = (array) $id;
424
        foreach (static::$ids as $j => $k) {
425
            $ids[$k] = $id[$j];
426
        }
427
428
        $model = new static($ids);
429
430
        return $model;
431
    }
432
433
    /**
434
     * Checks if the model has a property.
435
     *
436
     * @param string $property property
437
     *
438
     * @return bool has property
439
     */
440
    public static function hasProperty($property)
441
    {
442
        return in_array($property, static::$properties);
443
    }
444
445
    /**
446
     * Gets the mutator method name for a given proeprty name.
447
     * Looks for methods in the form of `setPropertyValue`.
448
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
449
     *
450
     * @param string $property property
451
     *
452
     * @return string|false method name if it exists
453
     */
454 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...
455
    {
456
        $class = get_called_class();
457
458
        $k = $class.':'.$property;
459
        if (!array_key_exists($k, self::$mutators)) {
460
            $inflector = Inflector::get();
461
            $method = 'set'.$inflector->camelize($property).'Value';
462
463
            if (!method_exists($class, $method)) {
464
                $method = false;
465
            }
466
467
            self::$mutators[$k] = $method;
468
        }
469
470
        return self::$mutators[$k];
471
    }
472
473
    /**
474
     * Gets the accessor method name for a given proeprty name.
475
     * Looks for methods in the form of `getPropertyValue`.
476
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
477
     *
478
     * @param string $property property
479
     *
480
     * @return string|false method name if it exists
481
     */
482 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...
483
    {
484
        $class = get_called_class();
485
486
        $k = $class.':'.$property;
487
        if (!array_key_exists($k, self::$accessors)) {
488
            $inflector = Inflector::get();
489
            $method = 'get'.$inflector->camelize($property).'Value';
490
491
            if (!method_exists($class, $method)) {
492
                $method = false;
493
            }
494
495
            self::$accessors[$k] = $method;
496
        }
497
498
        return self::$accessors[$k];
499
    }
500
501
    /**
502
     * Checks if a given property is a relationship.
503
     *
504
     * @param string $property
505
     *
506
     * @return bool
507
     */
508
    public static function isRelationship($property)
509
    {
510
        return in_array($property, static::$relationships);
511
    }
512
513
    /**
514
     * Gets the title of a property.
515
     *
516
     * @param string $name
517
     *
518
     * @return string
519
     */
520
    public static function getPropertyTitle($name)
521
    {
522
        // attmept to fetch the title from the Locale service
523
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
524
        if (self::$locale && $title = self::$locale->t($k)) {
525
            if ($title != $k) {
526
                return $title;
527
            }
528
        }
529
530
        return Inflector::get()->humanize($name);
531
    }
532
533
    /**
534
     * Gets the type cast for a property.
535
     *
536
     * @param string $name
537
     *
538
     * @return string|null
539
     */
540
    public static function getPropertyType($name)
541
    {
542
        if (property_exists(get_called_class(), 'casts')) {
543
            return array_value(static::$casts, $name);
544
        }
545
    }
546
547
    /**
548
     * Casts a value to a given type.
549
     *
550
     * @param string|null $type
551
     * @param mixed       $value
552
     *
553
     * @return mixed casted value
554
     */
555
    public static function cast($type, $value)
556
    {
557
        if ($value === null) {
558
            return;
559
        }
560
561
        switch ($type) {
562
        case self::TYPE_STRING:
563
            return (string) $value;
564
565
        case self::TYPE_INTEGER:
566
            return (int) $value;
567
568
        case self::TYPE_FLOAT:
569
            return (float) $value;
570
571
        case self::TYPE_BOOLEAN:
572
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
573
574
        case self::TYPE_DATE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
575
            // cast dates as unix timestamps
576
            if (!is_numeric($value)) {
577
                return strtotime($value);
578
            } else {
579
                return $value + 0;
580
            }
581
582 View Code Duplication
        case self::TYPE_ARRAY:
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...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
583
            // decode JSON into an array
584
            if (is_string($value)) {
585
                return json_decode($value, true);
586
            } else {
587
                return (array) $value;
588
            }
589
590 View Code Duplication
        case self::TYPE_OBJECT:
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...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
591
            // decode JSON into an object
592
            if (is_string($value)) {
593
                return (object) json_decode($value);
594
            } else {
595
                return (object) $value;
596
            }
597
598
        default:
599
            return $value;
600
        }
601
    }
602
603
    /////////////////////////////
604
    // Values
605
    /////////////////////////////
606
607
    /**
608
     * Sets an unsaved value.
609
     *
610
     * @param string $name
611
     * @param mixed  $value
612
     *
613
     * @throws BadMethodCallException when setting a relationship
614
     *
615
     * @return self
616
     */
617
    public function setValue($name, $value)
618
    {
619
        if (static::isRelationship($name)) {
620
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
621
        }
622
623
        // cast the value
624
        if ($type = static::getPropertyType($name)) {
625
            $value = static::cast($type, $value);
626
        }
627
628
        // set using any mutators
629
        if ($mutator = self::getMutator($name)) {
630
            $this->_unsaved[$name] = $this->$mutator($value);
631
        } else {
632
            $this->_unsaved[$name] = $value;
633
        }
634
635
        return $this;
636
    }
637
638
    /**
639
     * Sets a collection values on the model from an untrusted input.
640
     *
641
     * @param array $values
642
     *
643
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
644
     *
645
     * @return self
646
     */
647
    public function setValues($values)
648
    {
649
        // check if the model has a mass assignment whitelist
650
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
651
652
        // if no whitelist, then check for a blacklist
653
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
654
655
        foreach ($values as $k => $value) {
656
            // check for mass assignment violations
657
            if (($permitted && !in_array($k, $permitted)) ||
658
                ($protected && in_array($k, $protected))) {
659
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
660
            }
661
662
            $this->setValue($k, $value);
663
        }
664
665
        return $this;
666
    }
667
668
    /**
669
     * Ignores unsaved values when fetching the next value.
670
     *
671
     * @return self
672
     */
673
    public function ignoreUnsaved()
674
    {
675
        $this->_ignoreUnsaved = true;
676
677
        return $this;
678
    }
679
680
    /**
681
     * Gets property values from the model.
682
     *
683
     * This method looks up values from these locations in this
684
     * precedence order (least important to most important):
685
     *  1. local values
686
     *  2. unsaved values
687
     *
688
     * @param array $properties list of property names to fetch values of
689
     *
690
     * @return array
691
     *
692
     * @throws InvalidArgumentException when a property was requested not present in the values
693
     */
694
    public function get(array $properties)
695
    {
696
        // load the values from the local model cache
697
        $values = $this->_values;
698
699
        // unless specified, use any unsaved values
700
        $ignoreUnsaved = $this->_ignoreUnsaved;
701
        $this->_ignoreUnsaved = false;
702
        if (!$ignoreUnsaved) {
703
            $values = array_replace($values, $this->_unsaved);
704
        }
705
706
        // build the response
707
        $result = [];
708
        foreach ($properties as $k) {
709
            $accessor = self::getAccessor($k);
710
711
            // use the supplied value if it's available
712
            if (array_key_exists($k, $values)) {
713
                $result[$k] = $values[$k];
714
            // get relationship values
715
            } elseif (static::isRelationship($k)) {
716
                $result[$k] = $this->loadRelationship($k);
717
            // set any missing values to null
718
            } elseif (static::hasProperty($k)) {
719
                $result[$k] = $this->_values[$k] = null;
720
            // throw an exception for non-properties that do not
721
            // have an accessor
722
            } elseif (!$accessor) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $accessor of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
723
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
724
            // otherwise the value is considered null
725
            } else {
726
                $result[$k] = null;
727
            }
728
729
            // call any accessors
730
            if ($accessor) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $accessor of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
731
                $result[$k] = $this->$accessor($result[$k]);
732
            }
733
        }
734
735
        return $result;
736
    }
737
738
    /**
739
     * Converts the model to an array.
740
     *
741
     * @return array model array
742
     */
743
    public function toArray()
744
    {
745
        // build the list of properties to retrieve
746
        // remove any hidden properties
747
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
748
        $properties = array_diff(static::$properties, $hide);
749
750
        // add any appended properties
751
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
752
        $properties = array_merge($properties, $append);
753
754
        // get the values for the properties
755
        $result = $this->get($properties);
756
757
        // convert any models to arrays
758
        foreach ($result as &$value) {
759
            if ($value instanceof self) {
760
                $value = $value->toArray();
761
            }
762
        }
763
764
        return $result;
765
    }
766
767
    /////////////////////////////
768
    // Persistence
769
    /////////////////////////////
770
771
    /**
772
     * Saves the model.
773
     *
774
     * @return bool
775
     */
776
    public function save()
777
    {
778
        if (!$this->_persisted) {
779
            return $this->create();
780
        }
781
782
        return $this->set();
783
    }
784
785
    /**
786
     * Creates a new model.
787
     *
788
     * @param array $data optional key-value properties to set
789
     *
790
     * @return bool
791
     *
792
     * @throws BadMethodCallException when called on an existing model
793
     */
794
    public function create(array $data = [])
795
    {
796
        if ($this->_persisted) {
797
            throw new BadMethodCallException('Cannot call create() on an existing model');
798
        }
799
800
        // mass assign values passed into create()
801
        $this->setValues($data);
802
803
        // add in any preset values
804
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
805
806
        // dispatch the model.creating event
807
        $event = $this->dispatch(ModelEvent::CREATING);
808
        if ($event->isPropagationStopped()) {
809
            return false;
810
        }
811
812
        // validate the model
813
        if (!$this->valid()) {
814
            return false;
815
        }
816
817
        // build the insert array
818
        $insertValues = [];
819 View Code Duplication
        foreach ($this->_unsaved as $k => $value) {
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...
820
            // remove any non-existent properties
821
            if (!static::hasProperty($k)) {
822
                continue;
823
            }
824
825
            $insertValues[$k] = $value;
826
        }
827
828
        if (!self::getDriver()->createModel($this, $insertValues)) {
829
            return false;
830
        }
831
832
        // update the model with the persisted values and new ID(s)
833
        $newValues = array_replace(
834
            $insertValues,
835
            $this->getNewIds());
836
        $this->refreshWith($newValues);
837
838
        // dispatch the model.created event
839
        $event = $this->dispatch(ModelEvent::CREATED);
840
841
        return !$event->isPropagationStopped();
842
    }
843
844
    /**
845
     * Gets the IDs for a newly created model.
846
     *
847
     * @return string
848
     */
849
    protected function getNewIds()
850
    {
851
        $ids = [];
852
        foreach (static::$ids as $k) {
853
            // check if the ID property was already given,
854
            if (isset($this->_unsaved[$k])) {
855
                $ids[$k] = $this->_unsaved[$k];
856
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
857
            } else {
858
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
859
            }
860
        }
861
862
        return $ids;
863
    }
864
865
    /**
866
     * Updates the model.
867
     *
868
     * @param array $data optional key-value properties to set
869
     *
870
     * @return bool
871
     *
872
     * @throws BadMethodCallException when not called on an existing model
873
     */
874
    public function set(array $data = [])
875
    {
876
        if (!$this->_persisted) {
877
            throw new BadMethodCallException('Can only call set() on an existing model');
878
        }
879
880
        // mass assign values passed into set()
881
        $this->setValues($data);
882
883
        // not updating anything?
884
        if (count($this->_unsaved) === 0) {
885
            return true;
886
        }
887
888
        // dispatch the model.updating event
889
        $event = $this->dispatch(ModelEvent::UPDATING);
890
        if ($event->isPropagationStopped()) {
891
            return false;
892
        }
893
894
        // validate the model
895
        if (!$this->valid()) {
896
            return false;
897
        }
898
899
        // build the update array
900
        $updateValues = [];
901 View Code Duplication
        foreach ($this->_unsaved as $k => $value) {
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...
902
            // remove any non-existent properties
903
            if (!static::hasProperty($k)) {
904
                continue;
905
            }
906
907
            $updateValues[$k] = $value;
908
        }
909
910
        if (!self::getDriver()->updateModel($this, $updateValues)) {
911
            return false;
912
        }
913
914
        // update the model with the persisted values
915
        $this->refreshWith($updateValues);
916
917
        // dispatch the model.updated event
918
        $event = $this->dispatch(ModelEvent::UPDATED);
919
920
        return !$event->isPropagationStopped();
921
    }
922
923
    /**
924
     * Delete the model.
925
     *
926
     * @return bool success
927
     */
928
    public function delete()
929
    {
930
        if (!$this->_persisted) {
931
            throw new BadMethodCallException('Can only call delete() on an existing model');
932
        }
933
934
        // dispatch the model.deleting event
935
        $event = $this->dispatch(ModelEvent::DELETING);
936
        if ($event->isPropagationStopped()) {
937
            return false;
938
        }
939
940
        $deleted = self::getDriver()->deleteModel($this);
941
942
        if ($deleted) {
943
            // dispatch the model.deleted event
944
            $event = $this->dispatch(ModelEvent::DELETED);
945
            if ($event->isPropagationStopped()) {
946
                return false;
947
            }
948
949
            $this->_persisted = false;
950
        }
951
952
        return $deleted;
953
    }
954
955
    /**
956
     * Tells if the model has been persisted.
957
     *
958
     * @return bool
959
     */
960
    public function persisted()
961
    {
962
        return $this->_persisted;
963
    }
964
965
    /**
966
     * Loads the model from the data layer.
967
     *
968
     * @return self
969
     *
970
     * @throws NotFoundException
971
     */
972
    public function refresh()
973
    {
974
        if (!$this->_persisted) {
975
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
976
        }
977
978
        $query = static::query();
979
        $query->where($this->ids());
980
981
        $values = self::getDriver()->queryModels($query);
982
983
        if (count($values) === 0) {
984
            return $this;
985
        }
986
987
        return $this->refreshWith($values[0]);
988
    }
989
990
    /**
991
     * Loads values into the model retrieved from the data layer.
992
     *
993
     * @param array $values values
994
     *
995
     * @return self
996
     */
997
    public function refreshWith(array $values)
998
    {
999
        // cast the values
1000
        if (property_exists($this, 'casts')) {
1001
            foreach ($values as $k => &$value) {
1002
                if ($type = static::getPropertyType($k)) {
1003
                    $value = static::cast($type, $value);
1004
                }
1005
            }
1006
        }
1007
1008
        $this->_persisted = true;
1009
        $this->_values = $values;
1010
        $this->_unsaved = [];
1011
1012
        return $this;
1013
    }
1014
1015
    /////////////////////////////
1016
    // Queries
1017
    /////////////////////////////
1018
1019
    /**
1020
     * Generates a new query instance.
1021
     *
1022
     * @return Query
1023
     */
1024
    public static function query()
1025
    {
1026
        // Create a new model instance for the query to ensure
1027
        // that the model's initialize() method gets called.
1028
        // Otherwise, the property definitions will be incomplete.
1029
        $model = new static();
1030
1031
        return new Query($model);
1032
    }
1033
1034
    /**
1035
     * Finds a single instance of a model given it's ID.
1036
     *
1037
     * @param mixed $id
1038
     *
1039
     * @return Model|null
1040
     */
1041
    public static function find($id)
1042
    {
1043
        $model = static::buildFromId($id);
1044
1045
        return static::query()->where($model->ids())->first();
1046
    }
1047
1048
    /**
1049
     * Finds a single instance of a model given it's ID or throws an exception.
1050
     *
1051
     * @param mixed $id
1052
     *
1053
     * @return Model|false
1054
     *
1055
     * @throws NotFoundException when a model could not be found
1056
     */
1057
    public static function findOrFail($id)
1058
    {
1059
        $model = static::find($id);
1060
        if (!$model) {
1061
            throw new NotFoundException('Could not find the requested '.static::modelName());
1062
        }
1063
1064
        return $model;
1065
    }
1066
1067
    /**
1068
     * Gets the toal number of records matching an optional criteria.
1069
     *
1070
     * @param array $where criteria
1071
     *
1072
     * @return int total
1073
     */
1074
    public static function totalRecords(array $where = [])
1075
    {
1076
        $query = static::query();
1077
        $query->where($where);
1078
1079
        return self::getDriver()->totalRecords($query);
1080
    }
1081
1082
    /////////////////////////////
1083
    // Relationships
1084
    /////////////////////////////
1085
1086
    /**
1087
     * Creates the parent side of a One-To-One relationship.
1088
     *
1089
     * @param string $model      foreign model class
1090
     * @param string $foreignKey identifying key on foreign model
1091
     * @param string $localKey   identifying key on local model
1092
     *
1093
     * @return \Pulsar\Relation\Relation
1094
     */
1095 View Code Duplication
    public function hasOne($model, $foreignKey = '', $localKey = '')
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...
1096
    {
1097
        // the default local key would look like `user_id`
1098
        // for a model named User
1099
        if (!$foreignKey) {
1100
            $inflector = Inflector::get();
1101
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1102
        }
1103
1104
        if (!$localKey) {
1105
            $localKey = self::DEFAULT_ID_PROPERTY;
1106
        }
1107
1108
        return new HasOne($model, $foreignKey, $localKey, $this);
1109
    }
1110
1111
    /**
1112
     * Creates the child side of a One-To-One or One-To-Many relationship.
1113
     *
1114
     * @param string $model      foreign model class
1115
     * @param string $foreignKey identifying key on foreign model
1116
     * @param string $localKey   identifying key on local model
1117
     *
1118
     * @return \Pulsar\Relation\Relation
1119
     */
1120 View Code Duplication
    public function belongsTo($model, $foreignKey = '', $localKey = '')
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...
1121
    {
1122
        if (!$foreignKey) {
1123
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1124
        }
1125
1126
        // the default local key would look like `user_id`
1127
        // for a model named User
1128
        if (!$localKey) {
1129
            $inflector = Inflector::get();
1130
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1131
        }
1132
1133
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1134
    }
1135
1136
    /**
1137
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1138
     *
1139
     * @param string $model      foreign model class
1140
     * @param string $foreignKey identifying key on foreign model
1141
     * @param string $localKey   identifying key on local model
1142
     *
1143
     * @return \Pulsar\Relation\Relation
1144
     */
1145 View Code Duplication
    public function hasMany($model, $foreignKey = '', $localKey = '')
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...
1146
    {
1147
        // the default local key would look like `user_id`
1148
        // for a model named User
1149
        if (!$foreignKey) {
1150
            $inflector = Inflector::get();
1151
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1152
        }
1153
1154
        if (!$localKey) {
1155
            $localKey = self::DEFAULT_ID_PROPERTY;
1156
        }
1157
1158
        return new HasMany($model, $foreignKey, $localKey, $this);
1159
    }
1160
1161
    /**
1162
     * Creates the child side of a Many-To-Many relationship.
1163
     *
1164
     * @param string $model      foreign model class
1165
     * @param string $foreignKey identifying key on foreign model
1166
     * @param string $localKey   identifying key on local model
1167
     *
1168
     * @return \Pulsar\Relation\Relation
1169
     */
1170 View Code Duplication
    public function belongsToMany($model, $foreignKey = '', $localKey = '')
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...
1171
    {
1172
        if (!$foreignKey) {
1173
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1174
        }
1175
1176
        // the default local key would look like `user_id`
1177
        // for a model named User
1178
        if (!$localKey) {
1179
            $inflector = Inflector::get();
1180
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1181
        }
1182
1183
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1184
    }
1185
1186
    /**
1187
     * Loads a given relationship (if not already) and returns
1188
     * its results.
1189
     *
1190
     * @param string $name
1191
     *
1192
     * @return mixed
1193
     */
1194
    protected function loadRelationship($name)
1195
    {
1196
        if (!isset($this->_values[$name])) {
1197
            $relationship = $this->$name();
1198
            $this->_values[$name] = $relationship->getResults();
1199
        }
1200
1201
        return $this->_values[$name];
1202
    }
1203
1204
    /////////////////////////////
1205
    // Events
1206
    /////////////////////////////
1207
1208
    /**
1209
     * Gets the event dispatcher.
1210
     *
1211
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1212
     */
1213
    public static function getDispatcher($ignoreCache = false)
1214
    {
1215
        $class = get_called_class();
1216
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1217
            self::$dispatchers[$class] = new EventDispatcher();
1218
        }
1219
1220
        return self::$dispatchers[$class];
1221
    }
1222
1223
    /**
1224
     * Subscribes to a listener to an event.
1225
     *
1226
     * @param string   $event    event name
1227
     * @param callable $listener
1228
     * @param int      $priority optional priority, higher #s get called first
1229
     */
1230
    public static function listen($event, callable $listener, $priority = 0)
1231
    {
1232
        static::getDispatcher()->addListener($event, $listener, $priority);
1233
    }
1234
1235
    /**
1236
     * Adds a listener to the model.creating event.
1237
     *
1238
     * @param callable $listener
1239
     * @param int      $priority
1240
     */
1241
    public static function creating(callable $listener, $priority = 0)
1242
    {
1243
        static::listen(ModelEvent::CREATING, $listener, $priority);
1244
    }
1245
1246
    /**
1247
     * Adds a listener to the model.created event.
1248
     *
1249
     * @param callable $listener
1250
     * @param int      $priority
1251
     */
1252
    public static function created(callable $listener, $priority = 0)
1253
    {
1254
        static::listen(ModelEvent::CREATED, $listener, $priority);
1255
    }
1256
1257
    /**
1258
     * Adds a listener to the model.updating event.
1259
     *
1260
     * @param callable $listener
1261
     * @param int      $priority
1262
     */
1263
    public static function updating(callable $listener, $priority = 0)
1264
    {
1265
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1266
    }
1267
1268
    /**
1269
     * Adds a listener to the model.updated event.
1270
     *
1271
     * @param callable $listener
1272
     * @param int      $priority
1273
     */
1274
    public static function updated(callable $listener, $priority = 0)
1275
    {
1276
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1277
    }
1278
1279
    /**
1280
     * Adds a listener to the model.deleting event.
1281
     *
1282
     * @param callable $listener
1283
     * @param int      $priority
1284
     */
1285
    public static function deleting(callable $listener, $priority = 0)
1286
    {
1287
        static::listen(ModelEvent::DELETING, $listener, $priority);
1288
    }
1289
1290
    /**
1291
     * Adds a listener to the model.deleted event.
1292
     *
1293
     * @param callable $listener
1294
     * @param int      $priority
1295
     */
1296
    public static function deleted(callable $listener, $priority = 0)
1297
    {
1298
        static::listen(ModelEvent::DELETED, $listener, $priority);
1299
    }
1300
1301
    /**
1302
     * Dispatches an event.
1303
     *
1304
     * @param string $eventName
1305
     *
1306
     * @return ModelEvent
1307
     */
1308
    protected function dispatch($eventName)
1309
    {
1310
        $event = new ModelEvent($this);
1311
1312
        return static::getDispatcher()->dispatch($eventName, $event);
1313
    }
1314
1315
    /////////////////////////////
1316
    // Validation
1317
    /////////////////////////////
1318
1319
    /**
1320
     * Gets the error stack for this model instance. Used to
1321
     * keep track of validation errors.
1322
     *
1323
     * @return Errors
1324
     */
1325
    public function errors()
1326
    {
1327
        if (!$this->_errors) {
1328
            $this->_errors = new Errors($this, self::$locale);
1329
        }
1330
1331
        return $this->_errors;
1332
    }
1333
1334
    /**
1335
     * Checks if the model is valid in its current state.
1336
     *
1337
     * @return bool
1338
     */
1339
    public function valid()
1340
    {
1341
        // clear any previous errors
1342
        $this->errors()->clear();
1343
1344
        // run the validator against the model values
1345
        $validator = $this->getValidator();
1346
        $values = $this->_values + $this->_unsaved;
1347
        $validated = $validator->validate($values);
1348
1349
        // add back any modified unsaved values
1350
        foreach (array_keys($this->_unsaved) as $k) {
1351
            $this->_unsaved[$k] = $values[$k];
1352
        }
1353
1354
        return $validated;
1355
    }
1356
1357
    /**
1358
     * Gets a new validator instance for this model.
1359
     * 
1360
     * @return Validator
1361
     */
1362
    public function getValidator()
1363
    {
1364
        return new Validator(static::$validations, $this->errors());
1365
    }
1366
}
1367