Completed
Push — master ( ff13dd...500912 )
by Jared
02:35
created

Model::hasProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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