Completed
Push — master ( c0a70e...e5fdce )
by Jared
02:18
created

Model::initialize()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 18
rs 9.2
cc 4
eloc 7
nc 4
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
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
        'type' => self::TYPE_INTEGER,
118
    ];
119
120
    /**
121
     * @staticvar array
122
     */
123
    private static $timestampProperties = [
124
        'created_at' => [
125
            'type' => self::TYPE_DATE,
126
        ],
127
        'updated_at' => [
128
            'type' => self::TYPE_DATE,
129
        ],
130
    ];
131
132
    /**
133
     * @staticvar array
134
     */
135
    private static $timestampValidations = [
136
        'created_at' => 'timestamp|db_timestamp',
137
        'updated_at' => 'timestamp|db_timestamp',
138
    ];
139
140
    /**
141
     * @staticvar array
142
     */
143
    private static $initialized = [];
144
145
    /**
146
     * @staticvar DriverInterface
147
     */
148
    private static $driver;
149
150
    /**
151
     * @staticvar Locale
152
     */
153
    private static $locale;
154
155
    /**
156
     * @staticvar array
157
     */
158
    private static $accessors = [];
159
160
    /**
161
     * @staticvar array
162
     */
163
    private static $mutators = [];
164
165
    /**
166
     * @var bool
167
     */
168
    private $_ignoreUnsaved;
169
170
    /**
171
     * Creates a new model object.
172
     *
173
     * @param array $values values to fill model with
174
     */
175
    public function __construct(array $values = [])
176
    {
177
        $this->_values = $values;
178
        $this->app = self::$injectedApp;
179
180
        // ensure the initialize function is called only once
181
        $k = get_called_class();
182
        if (!isset(self::$initialized[$k])) {
183
            $this->initialize();
184
            self::$initialized[$k] = true;
185
        }
186
    }
187
188
    /**
189
     * The initialize() method is called once per model. It's used
190
     * to perform any one-off tasks before the model gets
191
     * constructed. This is a great place to add any model
192
     * properties. When extending this method be sure to call
193
     * parent::initialize() as some important stuff happens here.
194
     * If extending this method to add properties then you should
195
     * call parent::initialize() after adding any properties.
196
     */
197
    protected function initialize()
198
    {
199
        // add in the default ID property
200
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
201
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
202
        }
203
204
        // add in the auto timestamp properties
205
        if (property_exists(get_called_class(), 'autoTimestamps')) {
206
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
207
208
            static::$validations = array_replace(self::$timestampValidations, static::$validations);
209
        }
210
211
        // order the properties array by name for consistency
212
        // since it is constructed in a random order
213
        ksort(static::$properties);
214
    }
215
216
    /**
217
     * Injects a DI container.
218
     *
219
     * @param \Pimple\Container $app
220
     */
221
    public static function inject(Container $app)
222
    {
223
        self::$injectedApp = $app;
224
    }
225
226
    /**
227
     * Gets the DI container used for this model.
228
     *
229
     * @return \Pimple\Container
230
     */
231
    public function getApp()
232
    {
233
        return $this->app;
234
    }
235
236
    /**
237
     * Sets the driver for all models.
238
     *
239
     * @param DriverInterface $driver
240
     */
241
    public static function setDriver(DriverInterface $driver)
242
    {
243
        self::$driver = $driver;
244
    }
245
246
    /**
247
     * Gets the driver for all models.
248
     *
249
     * @return DriverInterface
250
     *
251
     * @throws DriverMissingException
252
     */
253
    public static function getDriver()
254
    {
255
        if (!self::$driver) {
256
            throw new DriverMissingException('A model driver has not been set yet.');
257
        }
258
259
        return self::$driver;
260
    }
261
262
    /**
263
     * Clears the driver for all models.
264
     */
265
    public static function clearDriver()
266
    {
267
        self::$driver = null;
268
    }
269
270
    /**
271
     * Sets the locale instance for all models.
272
     *
273
     * @param Locale $locale
274
     */
275
    public static function setLocale(Locale $locale)
276
    {
277
        self::$locale = $locale;
278
    }
279
280
    /**
281
     * Clears the locale for all models.
282
     */
283
    public static function clearLocale()
284
    {
285
        self::$locale = null;
286
    }
287
288
    /**
289
     * Gets the name of the model without namespacing.
290
     *
291
     * @return string
292
     */
293
    public static function modelName()
294
    {
295
        return explode('\\', get_called_class())[0];
296
    }
297
298
    /**
299
     * Gets the model ID.
300
     *
301
     * @return string|number|null ID
302
     */
303
    public function id()
304
    {
305
        $ids = $this->ids();
306
307
        // if a single ID then return it
308
        if (count($ids) === 1) {
309
            return reset($ids);
310
        }
311
312
        // if multiple IDs then return a comma-separated list
313
        return implode(',', $ids);
314
    }
315
316
    /**
317
     * Gets a key-value map of the model ID.
318
     *
319
     * @return array ID map
320
     */
321
    public function ids()
322
    {
323
        return $this->get(static::$ids);
324
    }
325
326
    /////////////////////////////
327
    // Magic Methods
328
    /////////////////////////////
329
330
    public function __toString()
331
    {
332
        return get_called_class().'('.$this->id().')';
333
    }
334
335
    public function __get($name)
336
    {
337
        return array_values($this->get([$name]))[0];
338
    }
339
340
    public function __set($name, $value)
341
    {
342
        $this->setValue($name, $value);
343
    }
344
345
    public function __isset($name)
346
    {
347
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
348
    }
349
350
    public function __unset($name)
351
    {
352
        if (static::isRelationship($name)) {
353
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
354
        }
355
356
        if (array_key_exists($name, $this->_unsaved)) {
357
            unset($this->_unsaved[$name]);
358
        }
359
    }
360
361
    public static function __callStatic($name, $parameters)
362
    {
363
        // Any calls to unkown static methods should be deferred to
364
        // the query. This allows calls like User::where()
365
        // 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...
366
        return call_user_func_array([static::query(), $name], $parameters);
367
    }
368
369
    /////////////////////////////
370
    // ArrayAccess Interface
371
    /////////////////////////////
372
373
    public function offsetExists($offset)
374
    {
375
        return isset($this->$offset);
376
    }
377
378
    public function offsetGet($offset)
379
    {
380
        return $this->$offset;
381
    }
382
383
    public function offsetSet($offset, $value)
384
    {
385
        $this->$offset = $value;
386
    }
387
388
    public function offsetUnset($offset)
389
    {
390
        unset($this->$offset);
391
    }
392
393
    /////////////////////////////
394
    // Property Definitions
395
    /////////////////////////////
396
397
    /**
398
     * Gets all the property definitions for the model.
399
     *
400
     * @return array key-value map of properties
401
     */
402
    public static function getProperties()
403
    {
404
        return static::$properties;
405
    }
406
407
    /**
408
     * Gets a property defition for the model.
409
     *
410
     * @param string $property property to lookup
411
     *
412
     * @return array|null property
413
     */
414
    public static function getProperty($property)
415
    {
416
        return array_value(static::$properties, $property);
417
    }
418
419
    /**
420
     * Gets the names of the model ID properties.
421
     *
422
     * @return array
423
     */
424
    public static function getIdProperties()
425
    {
426
        return static::$ids;
427
    }
428
429
    /**
430
     * Builds an existing model instance given a single ID value or
431
     * ordered array of ID values.
432
     *
433
     * @param mixed $id
434
     *
435
     * @return Model
436
     */
437
    public static function buildFromId($id)
438
    {
439
        $ids = [];
440
        $id = (array) $id;
441
        foreach (static::$ids as $j => $k) {
442
            $ids[$k] = $id[$j];
443
        }
444
445
        $model = new static($ids);
446
447
        return $model;
448
    }
449
450
    /**
451
     * Checks if the model has a property.
452
     *
453
     * @param string $property property
454
     *
455
     * @return bool has property
456
     */
457
    public static function hasProperty($property)
458
    {
459
        return isset(static::$properties[$property]);
460
    }
461
462
    /**
463
     * Gets the mutator method name for a given proeprty name.
464
     * Looks for methods in the form of `setPropertyValue`.
465
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
466
     *
467
     * @param string $property property
468
     *
469
     * @return string|false method name if it exists
470
     */
471 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...
472
    {
473
        $class = get_called_class();
474
475
        $k = $class.':'.$property;
476
        if (!array_key_exists($k, self::$mutators)) {
477
            $inflector = Inflector::get();
478
            $method = 'set'.$inflector->camelize($property).'Value';
479
480
            if (!method_exists($class, $method)) {
481
                $method = false;
482
            }
483
484
            self::$mutators[$k] = $method;
485
        }
486
487
        return self::$mutators[$k];
488
    }
489
490
    /**
491
     * Gets the accessor method name for a given proeprty name.
492
     * Looks for methods in the form of `getPropertyValue`.
493
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
494
     *
495
     * @param string $property property
496
     *
497
     * @return string|false method name if it exists
498
     */
499 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...
500
    {
501
        $class = get_called_class();
502
503
        $k = $class.':'.$property;
504
        if (!array_key_exists($k, self::$accessors)) {
505
            $inflector = Inflector::get();
506
            $method = 'get'.$inflector->camelize($property).'Value';
507
508
            if (!method_exists($class, $method)) {
509
                $method = false;
510
            }
511
512
            self::$accessors[$k] = $method;
513
        }
514
515
        return self::$accessors[$k];
516
    }
517
518
    /**
519
     * Checks if a given property is a relationship.
520
     *
521
     * @param string $property
522
     *
523
     * @return bool
524
     */
525
    public static function isRelationship($property)
526
    {
527
        return in_array($property, static::$relationships);
528
    }
529
530
    /**
531
     * Gets the title of a property.
532
     *
533
     * @param string $name
534
     *
535
     * @return string
536
     */
537
    public static function getPropertyTitle($name)
538
    {
539
        // attmept to fetch the title from the Locale service
540
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
541
        if (self::$locale && $title = self::$locale->t($k)) {
542
            if ($title != $k) {
543
                return $title;
544
            }
545
        }
546
547
        return Inflector::get()->humanize($name);
548
    }
549
550
    /**
551
     * Casts a value to a given type.
552
     *
553
     * @param string|null $type
554
     * @param mixed       $value
555
     *
556
     * @return mixed casted value
557
     */
558
    public static function cast($type, $value)
559
    {
560
        if ($value === null) {
561
            return;
562
        }
563
564
        switch ($type) {
565
        case self::TYPE_STRING:
566
            return (string) $value;
567
568
        case self::TYPE_INTEGER:
569
            return (int) $value;
570
571
        case self::TYPE_FLOAT:
572
            return (float) $value;
573
574
        case self::TYPE_BOOLEAN:
575
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
576
577
        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...
578
            // cast dates as unix timestamps
579
            if (!is_numeric($value)) {
580
                return strtotime($value);
581
            } else {
582
                return $value + 0;
583
            }
584
585 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...
586
            // decode JSON into an array
587
            if (is_string($value)) {
588
                return json_decode($value, true);
589
            } else {
590
                return (array) $value;
591
            }
592
593 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...
594
            // decode JSON into an object
595
            if (is_string($value)) {
596
                return (object) json_decode($value);
597
            } else {
598
                return (object) $value;
599
            }
600
601
        default:
602
            return $value;
603
        }
604
    }
605
606
    /////////////////////////////
607
    // Values
608
    /////////////////////////////
609
610
    /**
611
     * Sets an unsaved value.
612
     *
613
     * @param string $name
614
     * @param mixed  $value
615
     *
616
     * @throws BadMethodCallException when setting a relationship
617
     *
618
     * @return self
619
     */
620
    public function setValue($name, $value)
621
    {
622
        if (static::isRelationship($name)) {
623
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
624
        }
625
626
        // set using any mutators
627
        if ($mutator = self::getMutator($name)) {
628
            $this->_unsaved[$name] = $this->$mutator($value);
629
        } else {
630
            $this->_unsaved[$name] = $value;
631
        }
632
633
        return $this;
634
    }
635
636
    /**
637
     * Sets a collection values on the model from an untrusted input.
638
     *
639
     * @param array $values
640
     *
641
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
642
     *
643
     * @return self
644
     */
645
    public function setValues($values)
646
    {
647
        // check if the model has a mass assignment whitelist
648
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
649
650
        // if no whitelist, then check for a blacklist
651
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
652
653
        foreach ($values as $k => $value) {
654
            // check for mass assignment violations
655
            if (($permitted && !in_array($k, $permitted)) ||
656
                ($protected && in_array($k, $protected))) {
657
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
658
            }
659
660
            $this->setValue($k, $value);
661
        }
662
663
        return $this;
664
    }
665
666
    /**
667
     * Ignores unsaved values when fetching the next value.
668
     *
669
     * @return self
670
     */
671
    public function ignoreUnsaved()
672
    {
673
        $this->_ignoreUnsaved = true;
674
675
        return $this;
676
    }
677
678
    /**
679
     * Gets property values from the model.
680
     *
681
     * This method looks up values from these locations in this
682
     * precedence order (least important to most important):
683
     *  1. local values
684
     *  2. unsaved values
685
     *
686
     * @param array $properties list of property names to fetch values of
687
     *
688
     * @return array
689
     *
690
     * @throws InvalidArgumentException when a property was requested not present in the values
691
     */
692
    public function get(array $properties)
693
    {
694
        // load the values from the local model cache
695
        $values = $this->_values;
696
697
        // unless specified, use any unsaved values
698
        $ignoreUnsaved = $this->_ignoreUnsaved;
699
        $this->_ignoreUnsaved = false;
700
        if (!$ignoreUnsaved) {
701
            $values = array_replace($values, $this->_unsaved);
702
        }
703
704
        // build the response
705
        $result = [];
706
        foreach ($properties as $k) {
707
            $accessor = self::getAccessor($k);
708
709
            // use the supplied value if it's available
710
            if (array_key_exists($k, $values)) {
711
                $result[$k] = $values[$k];
712
            // get relationship values
713
            } elseif (static::isRelationship($k)) {
714
                $result[$k] = $this->loadRelationship($k);
715
            // set any missing values to null
716
            } elseif (static::hasProperty($k)) {
717
                $result[$k] = $this->_values[$k] = null;
718
            // throw an exception for non-properties that do not
719
            // have an accessor
720
            } 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...
721
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
722
            // otherwise the value is considered null
723
            } else {
724
                $result[$k] = null;
725
            }
726
727
            // call any accessors
728
            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...
729
                $result[$k] = $this->$accessor($result[$k]);
730
            }
731
        }
732
733
        return $result;
734
    }
735
736
    /**
737
     * Converts the model to an array.
738
     *
739
     * @return array model array
740
     */
741
    public function toArray()
742
    {
743
        // build the list of properties to retrieve
744
        $properties = array_keys(static::$properties);
745
746
        // remove any hidden properties
747
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
748
        $properties = array_diff($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
            $property = static::getProperty($k);
822
            if ($property === null) {
823
                continue;
824
            }
825
826
            $insertValues[$k] = $value;
827
        }
828
829
        if (!self::getDriver()->createModel($this, $insertValues)) {
830
            return false;
831
        }
832
833
        // update the model with the persisted values and new ID(s)
834
        $newValues = array_replace(
835
            $insertValues,
836
            $this->getNewIds());
837
        $this->refreshWith($newValues);
838
839
        // dispatch the model.created event
840
        $event = $this->dispatch(ModelEvent::CREATED);
841
842
        return !$event->isPropagationStopped();
843
    }
844
845
    /**
846
     * Gets the IDs for a newly created model.
847
     *
848
     * @return string
849
     */
850
    protected function getNewIds()
851
    {
852
        $ids = [];
853
        foreach (static::$ids as $k) {
854
            // check if the ID property was already given,
855
            $property = static::getProperty($k);
0 ignored issues
show
Unused Code introduced by
$property is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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