Completed
Push — master ( b31e17...a9e5d9 )
by Jared
02:30
created

Model::checkUniqueness()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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