Completed
Push — master ( 500912...f85444 )
by Jared
02:28
created

Model::getNewIds()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.2
cc 4
eloc 9
nc 3
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
     * @staticvar array
64
     */
65
    protected static $relationships = [];
66
67
    /**
68
     * @staticvar \Pimple\Container
69
     */
70
    protected static $injectedApp;
71
72
    /**
73
     * @staticvar array
74
     */
75
    protected static $dispatchers;
76
77
    /**
78
     * @var \Pimple\Container
79
     */
80
    protected $app;
81
82
    /**
83
     * @var array
84
     */
85
    protected $_values = [];
86
87
    /**
88
     * @var array
89
     */
90
    protected $_unsaved = [];
91
92
    /**
93
     * @var bool
94
     */
95
    protected $_persisted = false;
96
97
    /**
98
     * @var Errors
99
     */
100
    protected $_errors;
101
102
    /////////////////////////////
103
    // Base model variables
104
    /////////////////////////////
105
106
    /**
107
     * @staticvar array
108
     */
109
    private static $propertyDefinitionBase = [
110
        'type' => self::TYPE_STRING,
111
        'mutable' => self::MUTABLE,
112
        // TODO move these into validation rules
113
        'unique' => false,
114
    ];
115
116
    /**
117
     * @staticvar array
118
     */
119
    private static $defaultIDProperty = [
120
        'type' => self::TYPE_NUMBER,
121
        'mutable' => self::IMMUTABLE,
122
    ];
123
124
    /**
125
     * @staticvar array
126
     */
127
    private static $timestampProperties = [
128
        'created_at' => [
129
            'type' => self::TYPE_DATE,
130
            'default' => null,
131
            'validate' => 'skip_empty|timestamp|db_timestamp',
132
        ],
133
        'updated_at' => [
134
            'type' => self::TYPE_DATE,
135
            'validate' => 'skip_empty|timestamp|db_timestamp',
136
        ],
137
    ];
138
139
    /**
140
     * @staticvar array
141
     */
142
    private static $initialized = [];
143
144
    /**
145
     * @staticvar DriverInterface
146
     */
147
    private static $driver;
148
149
    /**
150
     * @staticvar Locale
151
     */
152
    private static $locale;
153
154
    /**
155
     * @staticvar array
156
     */
157
    private static $accessors = [];
158
159
    /**
160
     * @staticvar array
161
     */
162
    private static $mutators = [];
163
164
    /**
165
     * @var bool
166
     */
167
    private $_ignoreUnsaved;
168
169
    /**
170
     * Creates a new model object.
171
     *
172
     * @param array $values values to fill model with
173
     */
174
    public function __construct(array $values = [])
175
    {
176
        $this->_values = $values;
177
        $this->app = self::$injectedApp;
178
179
        // ensure the initialize function is called only once
180
        $k = get_called_class();
181
        if (!isset(self::$initialized[$k])) {
182
            $this->initialize();
183
            self::$initialized[$k] = true;
184
        }
185
    }
186
187
    /**
188
     * The initialize() method is called once per model. It's used
189
     * to perform any one-off tasks before the model gets
190
     * constructed. This is a great place to add any model
191
     * properties. When extending this method be sure to call
192
     * parent::initialize() as some important stuff happens here.
193
     * If extending this method to add properties then you should
194
     * call parent::initialize() after adding any properties.
195
     */
196
    protected function initialize()
197
    {
198
        // add in the default ID property
199
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
200
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
201
        }
202
203
        // add in the auto timestamp properties
204
        if (property_exists(get_called_class(), 'autoTimestamps')) {
205
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
206
        }
207
208
        // fill in each property by extending the property
209
        // definition base
210
        foreach (static::$properties as &$property) {
211
            $property = array_replace(self::$propertyDefinitionBase, $property);
212
        }
213
214
        // order the properties array by name for consistency
215
        // since it is constructed in a random order
216
        ksort(static::$properties);
217
    }
218
219
    /**
220
     * Injects a DI container.
221
     *
222
     * @param \Pimple\Container $app
223
     */
224
    public static function inject(Container $app)
225
    {
226
        self::$injectedApp = $app;
227
    }
228
229
    /**
230
     * Gets the DI container used for this model.
231
     *
232
     * @return \Pimple\Container
233
     */
234
    public function getApp()
235
    {
236
        return $this->app;
237
    }
238
239
    /**
240
     * Sets the driver for all models.
241
     *
242
     * @param DriverInterface $driver
243
     */
244
    public static function setDriver(DriverInterface $driver)
245
    {
246
        self::$driver = $driver;
247
    }
248
249
    /**
250
     * Gets the driver for all models.
251
     *
252
     * @return DriverInterface
253
     *
254
     * @throws DriverMissingException
255
     */
256
    public static function getDriver()
257
    {
258
        if (!self::$driver) {
259
            throw new DriverMissingException('A model driver has not been set yet.');
260
        }
261
262
        return self::$driver;
263
    }
264
265
    /**
266
     * Clears the driver for all models.
267
     */
268
    public static function clearDriver()
269
    {
270
        self::$driver = null;
271
    }
272
273
    /**
274
     * Sets the locale instance for all models.
275
     *
276
     * @param Locale $locale
277
     */
278
    public static function setLocale(Locale $locale)
279
    {
280
        self::$locale = $locale;
281
    }
282
283
    /**
284
     * Gets the name of the model without namespacing.
285
     *
286
     * @return string
287
     */
288
    public static function modelName()
289
    {
290
        return explode('\\', get_called_class())[0];
291
    }
292
293
    /**
294
     * Gets the model ID.
295
     *
296
     * @return string|number|null ID
297
     */
298
    public function id()
299
    {
300
        $ids = $this->ids();
301
302
        // if a single ID then return it
303
        if (count($ids) === 1) {
304
            return reset($ids);
305
        }
306
307
        // if multiple IDs then return a comma-separated list
308
        return implode(',', $ids);
309
    }
310
311
    /**
312
     * Gets a key-value map of the model ID.
313
     *
314
     * @return array ID map
315
     */
316
    public function ids()
317
    {
318
        return $this->get(static::$ids);
319
    }
320
321
    /////////////////////////////
322
    // Magic Methods
323
    /////////////////////////////
324
325
    public function __toString()
326
    {
327
        return get_called_class().'('.$this->id().')';
328
    }
329
330
    public function __get($name)
331
    {
332
        return array_values($this->get([$name]))[0];
333
    }
334
335
    public function __set($name, $value)
336
    {
337
        $this->setValue($name, $value);
338
    }
339
340
    public function __isset($name)
341
    {
342
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
343
    }
344
345
    public function __unset($name)
346
    {
347
        if (static::isRelationship($name)) {
348
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
349
        }
350
351
        if (array_key_exists($name, $this->_unsaved)) {
352
            unset($this->_unsaved[$name]);
353
        }
354
    }
355
356
    public static function __callStatic($name, $parameters)
357
    {
358
        // Any calls to unkown static methods should be deferred to
359
        // the query. This allows calls like User::where()
360
        // 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...
361
        return call_user_func_array([static::query(), $name], $parameters);
362
    }
363
364
    /////////////////////////////
365
    // ArrayAccess Interface
366
    /////////////////////////////
367
368
    public function offsetExists($offset)
369
    {
370
        return isset($this->$offset);
371
    }
372
373
    public function offsetGet($offset)
374
    {
375
        return $this->$offset;
376
    }
377
378
    public function offsetSet($offset, $value)
379
    {
380
        $this->$offset = $value;
381
    }
382
383
    public function offsetUnset($offset)
384
    {
385
        unset($this->$offset);
386
    }
387
388
    /////////////////////////////
389
    // Property Definitions
390
    /////////////////////////////
391
392
    /**
393
     * Gets all the property definitions for the model.
394
     *
395
     * @return array key-value map of properties
396
     */
397
    public static function getProperties()
398
    {
399
        return static::$properties;
400
    }
401
402
    /**
403
     * Gets a property defition for the model.
404
     *
405
     * @param string $property property to lookup
406
     *
407
     * @return array|null property
408
     */
409
    public static function getProperty($property)
410
    {
411
        return array_value(static::$properties, $property);
412
    }
413
414
    /**
415
     * Gets the names of the model ID properties.
416
     *
417
     * @return array
418
     */
419
    public static function getIdProperties()
420
    {
421
        return static::$ids;
422
    }
423
424
    /**
425
     * Builds an existing model instance given a single ID value or
426
     * ordered array of ID values.
427
     *
428
     * @param mixed $id
429
     *
430
     * @return Model
431
     */
432
    public static function buildFromId($id)
433
    {
434
        $ids = [];
435
        $id = (array) $id;
436
        foreach (static::$ids as $j => $k) {
437
            $ids[$k] = $id[$j];
438
        }
439
440
        $model = new static($ids);
441
442
        return $model;
443
    }
444
445
    /**
446
     * Checks if the model has a property.
447
     *
448
     * @param string $property property
449
     *
450
     * @return bool has property
451
     */
452
    public static function hasProperty($property)
453
    {
454
        return isset(static::$properties[$property]);
455
    }
456
457
    /**
458
     * Gets the mutator method name for a given proeprty name.
459
     * Looks for methods in the form of `setPropertyValue`.
460
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
461
     *
462
     * @param string $property property
463
     *
464
     * @return string|false method name if it exists
465
     */
466 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...
467
    {
468
        $class = get_called_class();
469
470
        $k = $class.':'.$property;
471
        if (!array_key_exists($k, self::$mutators)) {
472
            $inflector = Inflector::get();
473
            $method = 'set'.$inflector->camelize($property).'Value';
474
475
            if (!method_exists($class, $method)) {
476
                $method = false;
477
            }
478
479
            self::$mutators[$k] = $method;
480
        }
481
482
        return self::$mutators[$k];
483
    }
484
485
    /**
486
     * Gets the accessor method name for a given proeprty name.
487
     * Looks for methods in the form of `getPropertyValue`.
488
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
489
     *
490
     * @param string $property property
491
     *
492
     * @return string|false method name if it exists
493
     */
494 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...
495
    {
496
        $class = get_called_class();
497
498
        $k = $class.':'.$property;
499
        if (!array_key_exists($k, self::$accessors)) {
500
            $inflector = Inflector::get();
501
            $method = 'get'.$inflector->camelize($property).'Value';
502
503
            if (!method_exists($class, $method)) {
504
                $method = false;
505
            }
506
507
            self::$accessors[$k] = $method;
508
        }
509
510
        return self::$accessors[$k];
511
    }
512
513
    /**
514
     * Checks if a given property is a relationship.
515
     *
516
     * @param string $property
517
     *
518
     * @return bool
519
     */
520
    public static function isRelationship($property)
521
    {
522
        return in_array($property, static::$relationships);
523
    }
524
525
    /**
526
     * Gets the title of a property.
527
     *
528
     * @param string $name
529
     *
530
     * @return string
531
     */
532
    public static function getPropertyTitle($name)
533
    {
534
        // TODO the property title should be fetched from 
535
        // the pulsar.properties.$name value in locale
536
        $property = static::getProperty($name);
537
        if ($property && isset($property['title'])) {
538
            return $property['title'];
539
        }
540
541
        return Inflector::get()->humanize($name);
542
    }
543
544
    /**
545
     * Gets the default value for a property.
546
     *
547
     * @param string|array $property
548
     *
549
     * @return mixed
550
     */
551
    public static function getDefaultValueFor($property)
552
    {
553
        if (!is_array($property)) {
554
            $property = static::getProperty($property);
555
        }
556
557
        return $property ? array_value($property, 'default') : null;
558
    }
559
560
    /////////////////////////////
561
    // Values
562
    /////////////////////////////
563
564
    /**
565
     * Sets an unsaved value.
566
     *
567
     * @param string $name
568
     * @param mixed  $value
569
     *
570
     * @throws BadMethodCallException when setting a relationship
571
     */
572
    public function setValue($name, $value)
573
    {
574
        if (static::isRelationship($name)) {
575
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
576
        }
577
578
        // set using any mutators
579
        if ($mutator = self::getMutator($name)) {
580
            $this->_unsaved[$name] = $this->$mutator($value);
581
        } else {
582
            $this->_unsaved[$name] = $value;
583
        }
584
585
        return $this;
586
    }
587
588
    /**
589
     * Ignores unsaved values when fetching the next value.
590
     *
591
     * @return self
592
     */
593
    public function ignoreUnsaved()
594
    {
595
        $this->_ignoreUnsaved = true;
596
597
        return $this;
598
    }
599
600
    /**
601
     * Gets property values from the model.
602
     *
603
     * This method looks up values from these locations in this
604
     * precedence order (least important to most important):
605
     *  1. defaults
606
     *  2. local values
607
     *  3. unsaved values
608
     *
609
     * @param array $properties list of property names to fetch values of
610
     *
611
     * @return array
612
     *
613
     * @throws InvalidArgumentException when a property was requested not present in the values
614
     */
615
    public function get(array $properties)
616
    {
617
        // load the values from the local model cache
618
        $values = $this->_values;
619
620
        // unless specified, use any unsaved values
621
        $ignoreUnsaved = $this->_ignoreUnsaved;
622
        $this->_ignoreUnsaved = false;
623
        if (!$ignoreUnsaved) {
624
            $values = array_replace($values, $this->_unsaved);
625
        }
626
627
        // build the response
628
        $result = [];
629
        foreach ($properties as $k) {
630
            $accessor = self::getAccessor($k);
631
632
            // use the supplied value if it's available
633
            if (array_key_exists($k, $values)) {
634
                $result[$k] = $values[$k];
635
            // get relationship values
636
            } elseif (static::isRelationship($k)) {
637
                $result[$k] = $this->loadRelationship($k);
638
            // set any missing values to the default value
639
            } elseif ($property = static::getProperty($k)) {
640
                $result[$k] = $this->_values[$k] = self::getDefaultValueFor($property);
641
            // throw an exception for non-properties that do not
642
            // have an accessor
643
            } 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...
644
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
645
            // otherwise the value is considered null
646
            } else {
647
                $result[$k] = null;
648
            }
649
650
            // call any accessors
651
            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...
652
                $result[$k] = $this->$accessor($result[$k]);
653
            }
654
        }
655
656
        return $result;
657
    }
658
659
    /**
660
     * Converts the model to an array.
661
     *
662
     * @return array model array
663
     */
664
    public function toArray()
665
    {
666
        // build the list of properties to retrieve
667
        $properties = array_keys(static::$properties);
668
669
        // remove any hidden properties
670
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
671
        $properties = array_diff($properties, $hide);
672
673
        // add any appended properties
674
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
675
        $properties = array_merge($properties, $append);
676
677
        // get the values for the properties
678
        $result = $this->get($properties);
679
680
        // convert any models to arrays
681
        foreach ($result as &$value) {
682
            if ($value instanceof self) {
683
                $value = $value->toArray();
684
            }
685
        }
686
687
        return $result;
688
    }
689
690
    /////////////////////////////
691
    // Persistence
692
    /////////////////////////////
693
694
    /**
695
     * Saves the model.
696
     *
697
     * @return bool
698
     */
699
    public function save()
700
    {
701
        if (!$this->_persisted) {
702
            return $this->create();
703
        }
704
705
        return $this->set($this->_unsaved);
706
    }
707
708
    /**
709
     * Creates a new model.
710
     *
711
     * @param array $data optional key-value properties to set
712
     *
713
     * @return bool
714
     *
715
     * @throws BadMethodCallException when called on an existing model
716
     */
717
    public function create(array $data = [])
718
    {
719
        if ($this->_persisted) {
720
            throw new BadMethodCallException('Cannot call create() on an existing model');
721
        }
722
723
        if (!empty($data)) {
724
            foreach ($data as $k => $value) {
725
                $this->$k = $value;
726
            }
727
        }
728
729
        // dispatch the model.creating event
730
        $event = $this->dispatch(ModelEvent::CREATING);
731
        if ($event->isPropagationStopped()) {
732
            return false;
733
        }
734
735
        foreach (static::$properties as $name => $property) {
736
            // add in default values
737
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
738
                $this->_unsaved[$name] = $property['default'];
739
            }
740
        }
741
742
        // validate the model
743
        if (!$this->valid()) {
744
            return false;
745
        }
746
747
        // build the insert array
748
        $insertValues = [];
749
        foreach ($this->_unsaved as $k => $value) {
750
            // remove any non-existent or immutable properties
751
            $property = static::getProperty($k);
752
            if ($property === null || ($property['mutable'] == self::IMMUTABLE && $value !== self::getDefaultValueFor($property))) {
753
                continue;
754
            }
755
756
            $insertValues[$k] = $value;
757
        }
758
759
        if (!self::getDriver()->createModel($this, $insertValues)) {
760
            return false;
761
        }
762
763
        // determine the model's new ID
764
        $ids = $this->getNewIds();
765
766
        // NOTE clear the local cache before the model.created
767
        // event so that fetching values forces a reload
768
        // from the data layer
769
        $this->clearCache();
770
        $this->_values = $ids;
771
772
        // dispatch the model.created event
773
        $event = $this->dispatch(ModelEvent::CREATED);
774
        if ($event->isPropagationStopped()) {
775
            return false;
776
        }
777
778
        return true;
779
    }
780
781
    /**
782
     * Gets the IDs for a newly created model.
783
     *
784
     * @return string
785
     */
786
    protected function getNewIds()
787
    {
788
        $ids = [];
789
        foreach (static::$ids as $k) {
790
            // attempt use the supplied value if the ID property is mutable
791
            $property = static::getProperty($k);
792
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
793
                $ids[$k] = $this->_unsaved[$k];
794
            } else {
795
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
796
            }
797
        }
798
799
        return $ids;
800
    }
801
802
    /**
803
     * Updates the model.
804
     *
805
     * @param array $data optional key-value properties to set
806
     *
807
     * @return bool
808
     *
809
     * @throws BadMethodCallException when not called on an existing model
810
     */
811
    public function set(array $data = [])
812
    {
813
        if (!$this->_persisted) {
814
            throw new BadMethodCallException('Can only call set() on an existing model');
815
        }
816
817
        if (!empty($data)) {
818
            foreach ($data as $k => $value) {
819
                $this->$k = $value;
820
            }
821
        }
822
823
        // not updating anything?
824
        if (count($this->_unsaved) === 0) {
825
            return true;
826
        }
827
828
        // dispatch the model.updating event
829
        $event = $this->dispatch(ModelEvent::UPDATING);
830
        if ($event->isPropagationStopped()) {
831
            return false;
832
        }
833
834
        // validate the model
835
        if (!$this->valid()) {
836
            return false;
837
        }
838
839
        // build the update array
840
        $updateValues = [];
841
        foreach ($this->_unsaved as $k => $value) {
842
            // remove any non-existent or immutable properties
843
            $property = static::getProperty($k);
844
            if ($property === null || $property['mutable'] != self::MUTABLE) {
845
                continue;
846
            }
847
848
            $updateValues[$k] = $value;
849
        }
850
851
        if (!self::getDriver()->updateModel($this, $updateValues)) {
852
            return false;
853
        }
854
855
        // clear the local cache before the model.updated
856
        // event so that fetching values forces a reload
857
        // from the data layer
858
        $this->clearCache();
859
860
        // dispatch the model.updated event
861
        $event = $this->dispatch(ModelEvent::UPDATED);
862
        if ($event->isPropagationStopped()) {
863
            return false;
864
        }
865
866
        return true;
867
    }
868
869
    /**
870
     * Delete the model.
871
     *
872
     * @return bool success
873
     */
874
    public function delete()
875
    {
876
        if (!$this->_persisted) {
877
            throw new BadMethodCallException('Can only call delete() on an existing model');
878
        }
879
880
        // dispatch the model.deleting event
881
        $event = $this->dispatch(ModelEvent::DELETING);
882
        if ($event->isPropagationStopped()) {
883
            return false;
884
        }
885
886
        $deleted = self::getDriver()->deleteModel($this);
887
888
        if ($deleted) {
889
            // dispatch the model.deleted event
890
            $event = $this->dispatch(ModelEvent::DELETED);
891
            if ($event->isPropagationStopped()) {
892
                return false;
893
            }
894
895
            // NOTE clear the local cache before the model.deleted
896
            // event so that fetching values forces a reload
897
            // from the data layer
898
            $this->clearCache();
899
        }
900
901
        return $deleted;
902
    }
903
904
    /**
905
     * Tells if the model has been persisted.
906
     *
907
     * @return bool
908
     */
909
    public function persisted()
910
    {
911
        return $this->_persisted;
912
    }
913
914
    /**
915
     * Loads the model from the data layer.
916
     *
917
     * @return self
918
     */
919
    public function refresh()
920
    {
921
        if (!$this->_persisted) {
922
            return $this;
923
        }
924
925
        $query = static::query();
926
        $query->where($this->ids());
927
928
        $values = self::getDriver()->queryModels($query);
929
930
        if (count($values) === 0) {
931
            return $this;
932
        }
933
934
        return $this->refreshWith($values[0]);
935
    }
936
937
    /**
938
     * Loads values into the model retrieved from the data layer.
939
     *
940
     * @param array $values values
941
     *
942
     * @return self
943
     */
944
    public function refreshWith(array $values)
945
    {
946
        $this->_persisted = true;
947
        $this->_values = $values;
948
949
        return $this;
950
    }
951
952
    /**
953
     * Clears the cache for this model.
954
     *
955
     * @return self
956
     */
957
    public function clearCache()
958
    {
959
        $this->_unsaved = [];
960
        $this->_values = [];
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(get_called_class(), 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
1295
        $values = $this->_values + $this->_unsaved;
1296
        $validator = $this->getValidator($values);
1297
        $validated = $validator->validate($values);
1298
1299
        // check for unique values
1300
        // TODO this should be moved into a validation rule
1301
        if (!$this->checkUniqueness($values)) {
1302
            $validated = false;
1303
        }
1304
1305
        return $validated;
1306
    }
1307
1308
    /**
1309
     * Builds a validator for this model.
1310
     *
1311
     * @param array $values
1312
     * 
1313
     * @return Validator
1314
     */
1315
    private function getValidator(array $values)
0 ignored issues
show
Unused Code introduced by
The parameter $values is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1316
    {
1317
        // build a list of requirements for the validator
1318
        $requirements = [];
1319
        foreach (static::$properties as $name => $property) {
1320
            if (!isset($property['validate'])) {
1321
                continue;
1322
            }
1323
1324
            $requirements[$name] = $property['validate'];
1325
        }
1326
1327
        return new Validator($requirements, $this->errors());
1328
    }
1329
1330
    /**
1331
     * Checks if a value is unique for that property.
1332
     *
1333
     * @param array $values
1334
     *
1335
     * @return bool
1336
     */
1337
    private function checkUniqueness(array $values)
1338
    {
1339
        $isUnique = true;
1340
        foreach ($values as $name => $value) {
1341
            $property = static::getProperty($name);
1342
            if (!$property['unique'] || ($this->_persisted && $value == $this->ignoreUnsaved()->$name)) {
1343
                continue;
1344
            }
1345
1346
            if (static::totalRecords([$name => $value]) > 0) {
1347
                $this->errors()->add($name, 'pulsar.validation.unique');
1348
1349
                $isUnique = false;
1350
            }
1351
        }
1352
1353
        return $isUnique;
1354
    }
1355
}
1356