Completed
Push — master ( 6db224...24dc19 )
by Jared
02:42
created

Model::clearCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
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\NotFoundException;
20
use Pulsar\Relation\HasOne;
21
use Pulsar\Relation\BelongsTo;
22
use Pulsar\Relation\HasMany;
23
use Pulsar\Relation\BelongsToMany;
24
use Pimple\Container;
25
use Symfony\Component\EventDispatcher\EventDispatcher;
26
27
abstract class Model implements \ArrayAccess
28
{
29
    const IMMUTABLE = 0;
30
    const MUTABLE_CREATE_ONLY = 1;
31
    const MUTABLE = 2;
32
33
    const TYPE_STRING = 'string';
34
    const TYPE_NUMBER = 'number';
35
    const TYPE_BOOLEAN = 'boolean';
36
    const TYPE_DATE = 'date';
37
    const TYPE_OBJECT = 'object';
38
    const TYPE_ARRAY = 'array';
39
40
    const DEFAULT_ID_PROPERTY = 'id';
41
42
    /////////////////////////////
43
    // Model visible variables
44
    /////////////////////////////
45
46
    /**
47
     * List of model ID property names.
48
     *
49
     * @staticvar array
50
     */
51
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
52
53
    /**
54
     * Property definitions expressed as a key-value map with
55
     * property names as the keys.
56
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
57
     *
58
     * @staticvar array
59
     */
60
    protected static $properties = [];
61
62
    /**
63
     * Validation rules expressed as a key-value map with
64
     * property names as the keys.
65
     * i.e. ['name' => 'string:2'].
66
     *
67
     * @staticvar array
68
     */
69
    protected static $validations = [];
70
71
    /**
72
     * @staticvar array
73
     */
74
    protected static $relationships = [];
75
76
    /**
77
     * @staticvar \Pimple\Container
78
     */
79
    protected static $injectedApp;
80
81
    /**
82
     * @staticvar array
83
     */
84
    protected static $dispatchers;
85
86
    /**
87
     * @var \Pimple\Container
88
     */
89
    protected $app;
90
91
    /**
92
     * @var array
93
     */
94
    protected $_values = [];
95
96
    /**
97
     * @var array
98
     */
99
    protected $_unsaved = [];
100
101
    /**
102
     * @var bool
103
     */
104
    protected $_persisted = false;
105
106
    /**
107
     * @var Errors
108
     */
109
    protected $_errors;
110
111
    /////////////////////////////
112
    // Base model variables
113
    /////////////////////////////
114
115
    /**
116
     * @staticvar array
117
     */
118
    private static $propertyDefinitionBase = [
119
        'type' => self::TYPE_STRING,
120
        'mutable' => self::MUTABLE,
121
    ];
122
123
    /**
124
     * @staticvar array
125
     */
126
    private static $defaultIDProperty = [
127
        'type' => self::TYPE_NUMBER,
128
        'mutable' => self::IMMUTABLE,
129
    ];
130
131
    /**
132
     * @staticvar array
133
     */
134
    private static $timestampProperties = [
135
        'created_at' => [
136
            'type' => self::TYPE_DATE,
137
            'default' => null,
138
        ],
139
        'updated_at' => [
140
            'type' => self::TYPE_DATE,
141
        ],
142
    ];
143
144
    /**
145
     * @staticvar array
146
     */
147
    private static $timestampValidations = [
148
        'created_at' => 'timestamp|db_timestamp',
149
        'updated_at' => 'timestamp|db_timestamp',
150
    ];
151
152
    /**
153
     * @staticvar array
154
     */
155
    private static $initialized = [];
156
157
    /**
158
     * @staticvar DriverInterface
159
     */
160
    private static $driver;
161
162
    /**
163
     * @staticvar Locale
164
     */
165
    private static $locale;
166
167
    /**
168
     * @staticvar array
169
     */
170
    private static $accessors = [];
171
172
    /**
173
     * @staticvar array
174
     */
175
    private static $mutators = [];
176
177
    /**
178
     * @var bool
179
     */
180
    private $_ignoreUnsaved;
181
182
    /**
183
     * Creates a new model object.
184
     *
185
     * @param array $values values to fill model with
186
     */
187
    public function __construct(array $values = [])
188
    {
189
        $this->_values = $values;
190
        $this->app = self::$injectedApp;
191
192
        // ensure the initialize function is called only once
193
        $k = get_called_class();
194
        if (!isset(self::$initialized[$k])) {
195
            $this->initialize();
196
            self::$initialized[$k] = true;
197
        }
198
    }
199
200
    /**
201
     * The initialize() method is called once per model. It's used
202
     * to perform any one-off tasks before the model gets
203
     * constructed. This is a great place to add any model
204
     * properties. When extending this method be sure to call
205
     * parent::initialize() as some important stuff happens here.
206
     * If extending this method to add properties then you should
207
     * call parent::initialize() after adding any properties.
208
     */
209
    protected function initialize()
210
    {
211
        // add in the default ID property
212
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
213
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
214
        }
215
216
        // add in the auto timestamp properties
217
        if (property_exists(get_called_class(), 'autoTimestamps')) {
218
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
219
220
            static::$validations = array_replace(self::$timestampValidations, static::$validations);
221
        }
222
223
        // fill in each property by extending the property
224
        // definition base
225
        foreach (static::$properties as &$property) {
226
            $property = array_replace(self::$propertyDefinitionBase, $property);
227
        }
228
229
        // order the properties array by name for consistency
230
        // since it is constructed in a random order
231
        ksort(static::$properties);
232
    }
233
234
    /**
235
     * Injects a DI container.
236
     *
237
     * @param \Pimple\Container $app
238
     */
239
    public static function inject(Container $app)
240
    {
241
        self::$injectedApp = $app;
242
    }
243
244
    /**
245
     * Gets the DI container used for this model.
246
     *
247
     * @return \Pimple\Container
248
     */
249
    public function getApp()
250
    {
251
        return $this->app;
252
    }
253
254
    /**
255
     * Sets the driver for all models.
256
     *
257
     * @param DriverInterface $driver
258
     */
259
    public static function setDriver(DriverInterface $driver)
260
    {
261
        self::$driver = $driver;
262
    }
263
264
    /**
265
     * Gets the driver for all models.
266
     *
267
     * @return DriverInterface
268
     *
269
     * @throws DriverMissingException
270
     */
271
    public static function getDriver()
272
    {
273
        if (!self::$driver) {
274
            throw new DriverMissingException('A model driver has not been set yet.');
275
        }
276
277
        return self::$driver;
278
    }
279
280
    /**
281
     * Clears the driver for all models.
282
     */
283
    public static function clearDriver()
284
    {
285
        self::$driver = null;
286
    }
287
288
    /**
289
     * Sets the locale instance for all models.
290
     *
291
     * @param Locale $locale
292
     */
293
    public static function setLocale(Locale $locale)
294
    {
295
        self::$locale = $locale;
296
    }
297
298
    /**
299
     * Clears the locale for all models.
300
     */
301
    public static function clearLocale()
302
    {
303
        self::$locale = null;
304
    }
305
306
    /**
307
     * Gets the name of the model without namespacing.
308
     *
309
     * @return string
310
     */
311
    public static function modelName()
312
    {
313
        return explode('\\', get_called_class())[0];
314
    }
315
316
    /**
317
     * Gets the model ID.
318
     *
319
     * @return string|number|null ID
320
     */
321
    public function id()
322
    {
323
        $ids = $this->ids();
324
325
        // if a single ID then return it
326
        if (count($ids) === 1) {
327
            return reset($ids);
328
        }
329
330
        // if multiple IDs then return a comma-separated list
331
        return implode(',', $ids);
332
    }
333
334
    /**
335
     * Gets a key-value map of the model ID.
336
     *
337
     * @return array ID map
338
     */
339
    public function ids()
340
    {
341
        return $this->get(static::$ids);
342
    }
343
344
    /////////////////////////////
345
    // Magic Methods
346
    /////////////////////////////
347
348
    public function __toString()
349
    {
350
        return get_called_class().'('.$this->id().')';
351
    }
352
353
    public function __get($name)
354
    {
355
        return array_values($this->get([$name]))[0];
356
    }
357
358
    public function __set($name, $value)
359
    {
360
        $this->setValue($name, $value);
361
    }
362
363
    public function __isset($name)
364
    {
365
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
366
    }
367
368
    public function __unset($name)
369
    {
370
        if (static::isRelationship($name)) {
371
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
372
        }
373
374
        if (array_key_exists($name, $this->_unsaved)) {
375
            unset($this->_unsaved[$name]);
376
        }
377
    }
378
379
    public static function __callStatic($name, $parameters)
380
    {
381
        // Any calls to unkown static methods should be deferred to
382
        // the query. This allows calls like User::where()
383
        // 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...
384
        return call_user_func_array([static::query(), $name], $parameters);
385
    }
386
387
    /////////////////////////////
388
    // ArrayAccess Interface
389
    /////////////////////////////
390
391
    public function offsetExists($offset)
392
    {
393
        return isset($this->$offset);
394
    }
395
396
    public function offsetGet($offset)
397
    {
398
        return $this->$offset;
399
    }
400
401
    public function offsetSet($offset, $value)
402
    {
403
        $this->$offset = $value;
404
    }
405
406
    public function offsetUnset($offset)
407
    {
408
        unset($this->$offset);
409
    }
410
411
    /////////////////////////////
412
    // Property Definitions
413
    /////////////////////////////
414
415
    /**
416
     * Gets all the property definitions for the model.
417
     *
418
     * @return array key-value map of properties
419
     */
420
    public static function getProperties()
421
    {
422
        return static::$properties;
423
    }
424
425
    /**
426
     * Gets a property defition for the model.
427
     *
428
     * @param string $property property to lookup
429
     *
430
     * @return array|null property
431
     */
432
    public static function getProperty($property)
433
    {
434
        return array_value(static::$properties, $property);
435
    }
436
437
    /**
438
     * Gets the names of the model ID properties.
439
     *
440
     * @return array
441
     */
442
    public static function getIdProperties()
443
    {
444
        return static::$ids;
445
    }
446
447
    /**
448
     * Builds an existing model instance given a single ID value or
449
     * ordered array of ID values.
450
     *
451
     * @param mixed $id
452
     *
453
     * @return Model
454
     */
455
    public static function buildFromId($id)
456
    {
457
        $ids = [];
458
        $id = (array) $id;
459
        foreach (static::$ids as $j => $k) {
460
            $ids[$k] = $id[$j];
461
        }
462
463
        $model = new static($ids);
464
465
        return $model;
466
    }
467
468
    /**
469
     * Checks if the model has a property.
470
     *
471
     * @param string $property property
472
     *
473
     * @return bool has property
474
     */
475
    public static function hasProperty($property)
476
    {
477
        return isset(static::$properties[$property]);
478
    }
479
480
    /**
481
     * Gets the mutator method name for a given proeprty name.
482
     * Looks for methods in the form of `setPropertyValue`.
483
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
484
     *
485
     * @param string $property property
486
     *
487
     * @return string|false method name if it exists
488
     */
489 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...
490
    {
491
        $class = get_called_class();
492
493
        $k = $class.':'.$property;
494
        if (!array_key_exists($k, self::$mutators)) {
495
            $inflector = Inflector::get();
496
            $method = 'set'.$inflector->camelize($property).'Value';
497
498
            if (!method_exists($class, $method)) {
499
                $method = false;
500
            }
501
502
            self::$mutators[$k] = $method;
503
        }
504
505
        return self::$mutators[$k];
506
    }
507
508
    /**
509
     * Gets the accessor method name for a given proeprty name.
510
     * Looks for methods in the form of `getPropertyValue`.
511
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
512
     *
513
     * @param string $property property
514
     *
515
     * @return string|false method name if it exists
516
     */
517 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...
518
    {
519
        $class = get_called_class();
520
521
        $k = $class.':'.$property;
522
        if (!array_key_exists($k, self::$accessors)) {
523
            $inflector = Inflector::get();
524
            $method = 'get'.$inflector->camelize($property).'Value';
525
526
            if (!method_exists($class, $method)) {
527
                $method = false;
528
            }
529
530
            self::$accessors[$k] = $method;
531
        }
532
533
        return self::$accessors[$k];
534
    }
535
536
    /**
537
     * Checks if a given property is a relationship.
538
     *
539
     * @param string $property
540
     *
541
     * @return bool
542
     */
543
    public static function isRelationship($property)
544
    {
545
        return in_array($property, static::$relationships);
546
    }
547
548
    /**
549
     * Gets the title of a property.
550
     *
551
     * @param string $name
552
     *
553
     * @return string
554
     */
555
    public static function getPropertyTitle($name)
556
    {
557
        // attmept to fetch the title from the Locale service
558
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
559
        if (self::$locale && $title = self::$locale->t($k)) {
560
            if ($title != $k) {
561
                return $title;
562
            }
563
        }
564
565
        return Inflector::get()->humanize($name);
566
    }
567
568
    /**
569
     * Gets the default value for a property.
570
     *
571
     * @param string|array $property
572
     *
573
     * @return mixed
574
     */
575
    public static function getDefaultValueFor($property)
576
    {
577
        if (!is_array($property)) {
578
            $property = static::getProperty($property);
579
        }
580
581
        return $property ? array_value($property, 'default') : null;
582
    }
583
584
    /////////////////////////////
585
    // Values
586
    /////////////////////////////
587
588
    /**
589
     * Sets an unsaved value.
590
     *
591
     * @param string $name
592
     * @param mixed  $value
593
     *
594
     * @throws BadMethodCallException when setting a relationship
595
     */
596
    public function setValue($name, $value)
597
    {
598
        if (static::isRelationship($name)) {
599
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
600
        }
601
602
        // set using any mutators
603
        if ($mutator = self::getMutator($name)) {
604
            $this->_unsaved[$name] = $this->$mutator($value);
605
        } else {
606
            $this->_unsaved[$name] = $value;
607
        }
608
609
        return $this;
610
    }
611
612
    /**
613
     * Ignores unsaved values when fetching the next value.
614
     *
615
     * @return self
616
     */
617
    public function ignoreUnsaved()
618
    {
619
        $this->_ignoreUnsaved = true;
620
621
        return $this;
622
    }
623
624
    /**
625
     * Gets property values from the model.
626
     *
627
     * This method looks up values from these locations in this
628
     * precedence order (least important to most important):
629
     *  1. defaults
630
     *  2. local values
631
     *  3. unsaved values
632
     *
633
     * @param array $properties list of property names to fetch values of
634
     *
635
     * @return array
636
     *
637
     * @throws InvalidArgumentException when a property was requested not present in the values
638
     */
639
    public function get(array $properties)
640
    {
641
        // load the values from the local model cache
642
        $values = $this->_values;
643
644
        // unless specified, use any unsaved values
645
        $ignoreUnsaved = $this->_ignoreUnsaved;
646
        $this->_ignoreUnsaved = false;
647
        if (!$ignoreUnsaved) {
648
            $values = array_replace($values, $this->_unsaved);
649
        }
650
651
        // build the response
652
        $result = [];
653
        foreach ($properties as $k) {
654
            $accessor = self::getAccessor($k);
655
656
            // use the supplied value if it's available
657
            if (array_key_exists($k, $values)) {
658
                $result[$k] = $values[$k];
659
            // get relationship values
660
            } elseif (static::isRelationship($k)) {
661
                $result[$k] = $this->loadRelationship($k);
662
            // set any missing values to the default value
663
            } elseif ($property = static::getProperty($k)) {
664
                $result[$k] = $this->_values[$k] = self::getDefaultValueFor($property);
665
            // throw an exception for non-properties that do not
666
            // have an accessor
667
            } 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...
668
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
669
            // otherwise the value is considered null
670
            } else {
671
                $result[$k] = null;
672
            }
673
674
            // call any accessors
675
            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...
676
                $result[$k] = $this->$accessor($result[$k]);
677
            }
678
        }
679
680
        return $result;
681
    }
682
683
    /**
684
     * Converts the model to an array.
685
     *
686
     * @return array model array
687
     */
688
    public function toArray()
689
    {
690
        // build the list of properties to retrieve
691
        $properties = array_keys(static::$properties);
692
693
        // remove any hidden properties
694
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
695
        $properties = array_diff($properties, $hide);
696
697
        // add any appended properties
698
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
699
        $properties = array_merge($properties, $append);
700
701
        // get the values for the properties
702
        $result = $this->get($properties);
703
704
        // convert any models to arrays
705
        foreach ($result as &$value) {
706
            if ($value instanceof self) {
707
                $value = $value->toArray();
708
            }
709
        }
710
711
        return $result;
712
    }
713
714
    /////////////////////////////
715
    // Persistence
716
    /////////////////////////////
717
718
    /**
719
     * Saves the model.
720
     *
721
     * @return bool
722
     */
723
    public function save()
724
    {
725
        if (!$this->_persisted) {
726
            return $this->create();
727
        }
728
729
        return $this->set($this->_unsaved);
730
    }
731
732
    /**
733
     * Creates a new model.
734
     *
735
     * @param array $data optional key-value properties to set
736
     *
737
     * @return bool
738
     *
739
     * @throws BadMethodCallException when called on an existing model
740
     */
741
    public function create(array $data = [])
742
    {
743
        if ($this->_persisted) {
744
            throw new BadMethodCallException('Cannot call create() on an existing model');
745
        }
746
747
        if (!empty($data)) {
748
            foreach ($data as $k => $value) {
749
                $this->$k = $value;
750
            }
751
        }
752
753
        // dispatch the model.creating event
754
        $event = $this->dispatch(ModelEvent::CREATING);
755
        if ($event->isPropagationStopped()) {
756
            return false;
757
        }
758
759
        foreach (static::$properties as $name => $property) {
760
            // add in default values
761
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
762
                $this->_unsaved[$name] = $property['default'];
763
            }
764
        }
765
766
        // validate the model
767
        if (!$this->valid()) {
768
            return false;
769
        }
770
771
        // build the insert array
772
        $insertValues = [];
773
        foreach ($this->_unsaved as $k => $value) {
774
            // remove any non-existent or immutable properties
775
            $property = static::getProperty($k);
776
            if ($property === null || ($property['mutable'] == self::IMMUTABLE && $value !== self::getDefaultValueFor($property))) {
777
                continue;
778
            }
779
780
            $insertValues[$k] = $value;
781
        }
782
783
        if (!self::getDriver()->createModel($this, $insertValues)) {
784
            return false;
785
        }
786
787
        // update the model with the persisted values and new ID(s)
788
        $newValues = array_replace(
789
            $insertValues,
790
            $this->getNewIds());
791
        $this->refreshWith($newValues);
792
793
        // dispatch the model.created event
794
        $event = $this->dispatch(ModelEvent::CREATED);
795
796
        return !$event->isPropagationStopped();
797
    }
798
799
    /**
800
     * Gets the IDs for a newly created model.
801
     *
802
     * @return string
803
     */
804
    protected function getNewIds()
805
    {
806
        $ids = [];
807
        foreach (static::$ids as $k) {
808
            // attempt use the supplied value if the ID property is mutable
809
            $property = static::getProperty($k);
810
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
811
                $ids[$k] = $this->_unsaved[$k];
812
            } else {
813
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
814
            }
815
        }
816
817
        return $ids;
818
    }
819
820
    /**
821
     * Updates the model.
822
     *
823
     * @param array $data optional key-value properties to set
824
     *
825
     * @return bool
826
     *
827
     * @throws BadMethodCallException when not called on an existing model
828
     */
829
    public function set(array $data = [])
830
    {
831
        if (!$this->_persisted) {
832
            throw new BadMethodCallException('Can only call set() on an existing model');
833
        }
834
835
        if (!empty($data)) {
836
            foreach ($data as $k => $value) {
837
                $this->$k = $value;
838
            }
839
        }
840
841
        // not updating anything?
842
        if (count($this->_unsaved) === 0) {
843
            return true;
844
        }
845
846
        // dispatch the model.updating event
847
        $event = $this->dispatch(ModelEvent::UPDATING);
848
        if ($event->isPropagationStopped()) {
849
            return false;
850
        }
851
852
        // validate the model
853
        if (!$this->valid()) {
854
            return false;
855
        }
856
857
        // build the update array
858
        $updateValues = [];
859
        foreach ($this->_unsaved as $k => $value) {
860
            // remove any non-existent or immutable properties
861
            $property = static::getProperty($k);
862
            if ($property === null || $property['mutable'] != self::MUTABLE) {
863
                continue;
864
            }
865
866
            $updateValues[$k] = $value;
867
        }
868
869
        if (!self::getDriver()->updateModel($this, $updateValues)) {
870
            return false;
871
        }
872
873
        // update the model with the persisted values
874
        $this->refreshWith($updateValues);
875
876
        // dispatch the model.updated event
877
        $event = $this->dispatch(ModelEvent::UPDATED);
878
879
        return !$event->isPropagationStopped();
880
    }
881
882
    /**
883
     * Delete the model.
884
     *
885
     * @return bool success
886
     */
887
    public function delete()
888
    {
889
        if (!$this->_persisted) {
890
            throw new BadMethodCallException('Can only call delete() on an existing model');
891
        }
892
893
        // dispatch the model.deleting event
894
        $event = $this->dispatch(ModelEvent::DELETING);
895
        if ($event->isPropagationStopped()) {
896
            return false;
897
        }
898
899
        $deleted = self::getDriver()->deleteModel($this);
900
901
        if ($deleted) {
902
            // dispatch the model.deleted event
903
            $event = $this->dispatch(ModelEvent::DELETED);
904
            if ($event->isPropagationStopped()) {
905
                return false;
906
            }
907
908
            $this->_persisted = false;
909
        }
910
911
        return $deleted;
912
    }
913
914
    /**
915
     * Tells if the model has been persisted.
916
     *
917
     * @return bool
918
     */
919
    public function persisted()
920
    {
921
        return $this->_persisted;
922
    }
923
924
    /**
925
     * Loads the model from the data layer.
926
     *
927
     * @return self
928
     *
929
     * @throws NotFoundException
930
     */
931
    public function refresh()
932
    {
933
        if (!$this->_persisted) {
934
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
935
        }
936
937
        $query = static::query();
938
        $query->where($this->ids());
939
940
        $values = self::getDriver()->queryModels($query);
941
942
        if (count($values) === 0) {
943
            return $this;
944
        }
945
946
        return $this->refreshWith($values[0]);
947
    }
948
949
    /**
950
     * Loads values into the model retrieved from the data layer.
951
     *
952
     * @param array $values values
953
     *
954
     * @return self
955
     */
956
    public function refreshWith(array $values)
957
    {
958
        $this->_persisted = true;
959
        $this->_values = $values;
960
        $this->_unsaved = [];
961
962
        return $this;
963
    }
964
965
    /////////////////////////////
966
    // Queries
967
    /////////////////////////////
968
969
    /**
970
     * Generates a new query instance.
971
     *
972
     * @return Query
973
     */
974
    public static function query()
975
    {
976
        // Create a new model instance for the query to ensure
977
        // that the model's initialize() method gets called.
978
        // Otherwise, the property definitions will be incomplete.
979
        $model = new static();
980
981
        return new Query($model);
982
    }
983
984
    /**
985
     * Finds a single instance of a model given it's ID.
986
     *
987
     * @param mixed $id
988
     *
989
     * @return Model|null
990
     */
991
    public static function find($id)
992
    {
993
        $model = static::buildFromId($id);
994
995
        return static::query()->where($model->ids())->first();
996
    }
997
998
    /**
999
     * Finds a single instance of a model given it's ID or throws an exception.
1000
     *
1001
     * @param mixed $id
1002
     *
1003
     * @return Model|false
1004
     *
1005
     * @throws NotFoundException when a model could not be found
1006
     */
1007
    public static function findOrFail($id)
1008
    {
1009
        $model = static::find($id);
1010
        if (!$model) {
1011
            throw new NotFoundException('Could not find the requested '.static::modelName());
1012
        }
1013
1014
        return $model;
1015
    }
1016
1017
    /**
1018
     * Gets the toal number of records matching an optional criteria.
1019
     *
1020
     * @param array $where criteria
1021
     *
1022
     * @return int total
1023
     */
1024
    public static function totalRecords(array $where = [])
1025
    {
1026
        $query = static::query();
1027
        $query->where($where);
1028
1029
        return self::getDriver()->totalRecords($query);
1030
    }
1031
1032
    /////////////////////////////
1033
    // Relationships
1034
    /////////////////////////////
1035
1036
    /**
1037
     * Creates the parent side of a One-To-One relationship.
1038
     *
1039
     * @param string $model      foreign model class
1040
     * @param string $foreignKey identifying key on foreign model
1041
     * @param string $localKey   identifying key on local model
1042
     *
1043
     * @return \Pulsar\Relation\Relation
1044
     */
1045 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...
1046
    {
1047
        // the default local key would look like `user_id`
1048
        // for a model named User
1049
        if (!$foreignKey) {
1050
            $inflector = Inflector::get();
1051
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1052
        }
1053
1054
        if (!$localKey) {
1055
            $localKey = self::DEFAULT_ID_PROPERTY;
1056
        }
1057
1058
        return new HasOne($model, $foreignKey, $localKey, $this);
1059
    }
1060
1061
    /**
1062
     * Creates the child side of a One-To-One or One-To-Many relationship.
1063
     *
1064
     * @param string $model      foreign model class
1065
     * @param string $foreignKey identifying key on foreign model
1066
     * @param string $localKey   identifying key on local model
1067
     *
1068
     * @return \Pulsar\Relation\Relation
1069
     */
1070 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...
1071
    {
1072
        if (!$foreignKey) {
1073
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1074
        }
1075
1076
        // the default local key would look like `user_id`
1077
        // for a model named User
1078
        if (!$localKey) {
1079
            $inflector = Inflector::get();
1080
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1081
        }
1082
1083
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1084
    }
1085
1086
    /**
1087
     * Creates the parent side of a Many-To-One or Many-To-Many 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 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...
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 HasMany($model, $foreignKey, $localKey, $this);
1109
    }
1110
1111
    /**
1112
     * Creates the child side of a Many-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 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...
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 BelongsToMany($model, $foreignKey, $localKey, $this);
1134
    }
1135
1136
    /**
1137
     * Loads a given relationship (if not already) and returns
1138
     * its results.
1139
     *
1140
     * @param string $name
1141
     *
1142
     * @return mixed
1143
     */
1144
    protected function loadRelationship($name)
1145
    {
1146
        if (!isset($this->_values[$name])) {
1147
            $relationship = $this->$name();
1148
            $this->_values[$name] = $relationship->getResults();
1149
        }
1150
1151
        return $this->_values[$name];
1152
    }
1153
1154
    /////////////////////////////
1155
    // Events
1156
    /////////////////////////////
1157
1158
    /**
1159
     * Gets the event dispatcher.
1160
     *
1161
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1162
     */
1163
    public static function getDispatcher($ignoreCache = false)
1164
    {
1165
        $class = get_called_class();
1166
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1167
            self::$dispatchers[$class] = new EventDispatcher();
1168
        }
1169
1170
        return self::$dispatchers[$class];
1171
    }
1172
1173
    /**
1174
     * Subscribes to a listener to an event.
1175
     *
1176
     * @param string   $event    event name
1177
     * @param callable $listener
1178
     * @param int      $priority optional priority, higher #s get called first
1179
     */
1180
    public static function listen($event, callable $listener, $priority = 0)
1181
    {
1182
        static::getDispatcher()->addListener($event, $listener, $priority);
1183
    }
1184
1185
    /**
1186
     * Adds a listener to the model.creating event.
1187
     *
1188
     * @param callable $listener
1189
     * @param int      $priority
1190
     */
1191
    public static function creating(callable $listener, $priority = 0)
1192
    {
1193
        static::listen(ModelEvent::CREATING, $listener, $priority);
1194
    }
1195
1196
    /**
1197
     * Adds a listener to the model.created event.
1198
     *
1199
     * @param callable $listener
1200
     * @param int      $priority
1201
     */
1202
    public static function created(callable $listener, $priority = 0)
1203
    {
1204
        static::listen(ModelEvent::CREATED, $listener, $priority);
1205
    }
1206
1207
    /**
1208
     * Adds a listener to the model.updating event.
1209
     *
1210
     * @param callable $listener
1211
     * @param int      $priority
1212
     */
1213
    public static function updating(callable $listener, $priority = 0)
1214
    {
1215
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1216
    }
1217
1218
    /**
1219
     * Adds a listener to the model.updated event.
1220
     *
1221
     * @param callable $listener
1222
     * @param int      $priority
1223
     */
1224
    public static function updated(callable $listener, $priority = 0)
1225
    {
1226
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1227
    }
1228
1229
    /**
1230
     * Adds a listener to the model.deleting event.
1231
     *
1232
     * @param callable $listener
1233
     * @param int      $priority
1234
     */
1235
    public static function deleting(callable $listener, $priority = 0)
1236
    {
1237
        static::listen(ModelEvent::DELETING, $listener, $priority);
1238
    }
1239
1240
    /**
1241
     * Adds a listener to the model.deleted event.
1242
     *
1243
     * @param callable $listener
1244
     * @param int      $priority
1245
     */
1246
    public static function deleted(callable $listener, $priority = 0)
1247
    {
1248
        static::listen(ModelEvent::DELETED, $listener, $priority);
1249
    }
1250
1251
    /**
1252
     * Dispatches an event.
1253
     *
1254
     * @param string $eventName
1255
     *
1256
     * @return ModelEvent
1257
     */
1258
    protected function dispatch($eventName)
1259
    {
1260
        $event = new ModelEvent($this);
1261
1262
        return static::getDispatcher()->dispatch($eventName, $event);
1263
    }
1264
1265
    /////////////////////////////
1266
    // Validation
1267
    /////////////////////////////
1268
1269
    /**
1270
     * Gets the error stack for this model instance. Used to
1271
     * keep track of validation errors.
1272
     *
1273
     * @return Errors
1274
     */
1275
    public function errors()
1276
    {
1277
        if (!$this->_errors) {
1278
            $this->_errors = new Errors($this, self::$locale);
1279
        }
1280
1281
        return $this->_errors;
1282
    }
1283
1284
    /**
1285
     * Checks if the model is valid in its current state.
1286
     *
1287
     * @return bool
1288
     */
1289
    public function valid()
1290
    {
1291
        // clear any previous errors
1292
        $this->errors()->clear();
1293
1294
        // run the validator against the model values
1295
        $validator = $this->getValidator();
1296
        $values = $this->_values + $this->_unsaved;
1297
        $validated = $validator->validate($values);
1298
1299
        // add back any modified unsaved values
1300
        foreach (array_keys($this->_unsaved) as $k) {
1301
            $this->_unsaved[$k] = $values[$k];
1302
        }
1303
1304
        return $validated;
1305
    }
1306
1307
    /**
1308
     * Gets a new validator instance for this model.
1309
     * 
1310
     * @return Validator
1311
     */
1312
    public function getValidator()
1313
    {
1314
        return new Validator(static::$validations, $this->errors());
1315
    }
1316
}
1317