Completed
Push — master ( 101c92...f21d6d )
by Jared
24:11 queued 15:23
created

Model::getProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
namespace Pulsar;
12
13
use BadMethodCallException;
14
use ICanBoogie\Inflector;
15
use Infuse\Locale;
16
use InvalidArgumentException;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Exception\DriverMissingException;
19
use Pulsar\Exception\MassAssignmentException;
20
use Pulsar\Exception\NotFoundException;
21
use Pulsar\Relation\HasOne;
22
use Pulsar\Relation\BelongsTo;
23
use Pulsar\Relation\HasMany;
24
use Pulsar\Relation\BelongsToMany;
25
use Pimple\Container;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
28
abstract class Model implements \ArrayAccess
29
{
30
    const TYPE_STRING = 'string';
31
    const TYPE_INTEGER = 'integer';
32
    const TYPE_FLOAT = 'float';
33
    const TYPE_BOOLEAN = 'boolean';
34
    const TYPE_DATE = 'date';
35
    const TYPE_OBJECT = 'object';
36
    const TYPE_ARRAY = 'array';
37
38
    const DEFAULT_ID_PROPERTY = 'id';
39
40
    /////////////////////////////
41
    // Model visible variables
42
    /////////////////////////////
43
44
    /**
45
     * List of model ID property names.
46
     *
47
     * @staticvar array
48
     */
49
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
50
51
    /**
52
     * Validation rules expressed as a key-value map with
53
     * property names as the keys.
54
     * i.e. ['name' => 'string:2'].
55
     *
56
     * @staticvar array
57
     */
58
    protected static $validations = [];
59
60
    /**
61
     * @staticvar array
62
     */
63
    protected static $relationships = [];
64
65
    /**
66
     * @staticvar \Pimple\Container
67
     */
68
    protected static $injectedApp;
69
70
    /**
71
     * @staticvar array
72
     */
73
    protected static $dispatchers;
74
75
    /**
76
     * @var \Pimple\Container
77
     */
78
    protected $app;
79
80
    /**
81
     * @var array
82
     */
83
    protected $_values = [];
84
85
    /**
86
     * @var array
87
     */
88
    protected $_unsaved = [];
89
90
    /**
91
     * @var bool
92
     */
93
    protected $_persisted = false;
94
95
    /**
96
     * @var Errors
97
     */
98
    protected $_errors;
99
100
    /////////////////////////////
101
    // Base model variables
102
    /////////////////////////////
103
104
    /**
105
     * @staticvar array
106
     */
107
    private static $defaultIDProperty = [
108
        self::DEFAULT_ID_PROPERTY => self::TYPE_INTEGER,
109
    ];
110
111
    /**
112
     * @staticvar array
113
     */
114
    private static $timestampProperties = [
115
        'created_at' => self::TYPE_DATE,
116
        'updated_at' => self::TYPE_DATE,
117
    ];
118
119
    /**
120
     * @staticvar array
121
     */
122
    private static $timestampValidations = [
123
        'created_at' => 'timestamp|db_timestamp',
124
        'updated_at' => 'timestamp|db_timestamp',
125
    ];
126
127
    /**
128
     * @staticvar array
129
     */
130
    private static $initialized = [];
131
132
    /**
133
     * @staticvar DriverInterface
134
     */
135
    private static $driver;
136
137
    /**
138
     * @staticvar Locale
139
     */
140
    private static $locale;
141
142
    /**
143
     * @staticvar array
144
     */
145
    private static $accessors = [];
146
147
    /**
148
     * @staticvar array
149
     */
150
    private static $mutators = [];
151
152
    /**
153
     * @var bool
154
     */
155
    private $_ignoreUnsaved;
156
157
    /**
158
     * Creates a new model object.
159
     *
160
     * @param array $values values to fill model with
161
     */
162
    public function __construct(array $values = [])
163
    {
164
        $this->_values = $values;
165
        $this->app = self::$injectedApp;
166
167
        // ensure the initialize function is called only once
168
        $k = get_called_class();
169
        if (!isset(self::$initialized[$k])) {
170
            $this->initialize();
171
            self::$initialized[$k] = true;
172
        }
173
    }
174
175
    /**
176
     * The initialize() method is called once per model. It's used
177
     * to perform any one-off tasks before the model gets
178
     * constructed. This is a great place to add any model
179
     * properties. When extending this method be sure to call
180
     * parent::initialize() as some important stuff happens here.
181
     * If extending this method to add properties then you should
182
     * call parent::initialize() after adding any properties.
183
     */
184
    protected function initialize()
185
    {
186
        // add in the default ID property
187
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
188
            if (property_exists($this, 'casts')) {
189
                static::$casts = array_replace(self::$defaultIDProperty, static::$casts);
190
            }
191
        }
192
193
        // add in the auto timestamp properties
194
        if (property_exists($this, 'autoTimestamps')) {
195
            if (property_exists($this, 'casts')) {
196
                static::$casts = array_replace(self::$timestampProperties, static::$casts);
197
            }
198
199
            static::$validations = array_replace(self::$timestampValidations, static::$validations);
200
        }
201
    }
202
203
    /**
204
     * Injects a DI container.
205
     *
206
     * @param \Pimple\Container $app
207
     */
208
    public static function inject(Container $app)
209
    {
210
        self::$injectedApp = $app;
211
    }
212
213
    /**
214
     * Gets the DI container used for this model.
215
     *
216
     * @return \Pimple\Container
217
     */
218
    public function getApp()
219
    {
220
        return $this->app;
221
    }
222
223
    /**
224
     * Sets the driver for all models.
225
     *
226
     * @param DriverInterface $driver
227
     */
228
    public static function setDriver(DriverInterface $driver)
229
    {
230
        self::$driver = $driver;
231
    }
232
233
    /**
234
     * Gets the driver for all models.
235
     *
236
     * @return DriverInterface
237
     *
238
     * @throws DriverMissingException
239
     */
240
    public static function getDriver()
241
    {
242
        if (!self::$driver) {
243
            throw new DriverMissingException('A model driver has not been set yet.');
244
        }
245
246
        return self::$driver;
247
    }
248
249
    /**
250
     * Clears the driver for all models.
251
     */
252
    public static function clearDriver()
253
    {
254
        self::$driver = null;
255
    }
256
257
    /**
258
     * Sets the locale instance for all models.
259
     *
260
     * @param Locale $locale
261
     */
262
    public static function setLocale(Locale $locale)
263
    {
264
        self::$locale = $locale;
265
    }
266
267
    /**
268
     * Clears the locale for all models.
269
     */
270
    public static function clearLocale()
271
    {
272
        self::$locale = null;
273
    }
274
275
    /**
276
     * Gets the name of the model without namespacing.
277
     *
278
     * @return string
279
     */
280
    public static function modelName()
281
    {
282
        return explode('\\', get_called_class())[0];
283
    }
284
285
    /**
286
     * Gets the model ID.
287
     *
288
     * @return string|number|null ID
289
     */
290
    public function id()
291
    {
292
        $ids = $this->ids();
293
294
        // if a single ID then return it
295
        if (count($ids) === 1) {
296
            return reset($ids);
297
        }
298
299
        // if multiple IDs then return a comma-separated list
300
        return implode(',', $ids);
301
    }
302
303
    /**
304
     * Gets a key-value map of the model ID.
305
     *
306
     * @return array ID map
307
     */
308
    public function ids()
309
    {
310
        return $this->get(static::$ids);
311
    }
312
313
    /////////////////////////////
314
    // Magic Methods
315
    /////////////////////////////
316
317
    public function __toString()
318
    {
319
        return get_called_class().'('.$this->id().')';
320
    }
321
322
    public function __get($name)
323
    {
324
        return array_values($this->get([$name]))[0];
325
    }
326
327
    public function __set($name, $value)
328
    {
329
        $this->setValue($name, $value);
330
    }
331
332
    public function __isset($name)
333
    {
334
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
335
    }
336
337
    public function __unset($name)
338
    {
339
        if (static::isRelationship($name)) {
340
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
341
        }
342
343
        if (array_key_exists($name, $this->_unsaved)) {
344
            unset($this->_unsaved[$name]);
345
        }
346
    }
347
348
    public static function __callStatic($name, $parameters)
349
    {
350
        // Any calls to unkown static methods should be deferred to
351
        // the query. This allows calls like User::where()
352
        // 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...
353
        return call_user_func_array([static::query(), $name], $parameters);
354
    }
355
356
    /////////////////////////////
357
    // ArrayAccess Interface
358
    /////////////////////////////
359
360
    public function offsetExists($offset)
361
    {
362
        return isset($this->$offset);
363
    }
364
365
    public function offsetGet($offset)
366
    {
367
        return $this->$offset;
368
    }
369
370
    public function offsetSet($offset, $value)
371
    {
372
        $this->$offset = $value;
373
    }
374
375
    public function offsetUnset($offset)
376
    {
377
        unset($this->$offset);
378
    }
379
380
    /////////////////////////////
381
    // Property Definitions
382
    /////////////////////////////
383
384
    /**
385
     * Gets the names of the model ID properties.
386
     *
387
     * @return array
388
     */
389
    public static function getIdProperties()
390
    {
391
        return static::$ids;
392
    }
393
394
    /**
395
     * Builds an existing model instance given a single ID value or
396
     * ordered array of ID values.
397
     *
398
     * @param mixed $id
399
     *
400
     * @return Model
401
     */
402
    public static function buildFromId($id)
403
    {
404
        $ids = [];
405
        $id = (array) $id;
406
        foreach (static::$ids as $j => $k) {
407
            $ids[$k] = $id[$j];
408
        }
409
410
        $model = new static($ids);
411
412
        return $model;
413
    }
414
415
    /**
416
     * Gets the mutator method name for a given proeprty name.
417
     * Looks for methods in the form of `setPropertyValue`.
418
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
419
     *
420
     * @param string $property
421
     *
422
     * @return string|false method name if it exists
423
     */
424 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...
425
    {
426
        $class = get_called_class();
427
428
        $k = $class.':'.$property;
429
        if (!array_key_exists($k, self::$mutators)) {
430
            $inflector = Inflector::get();
431
            $method = 'set'.$inflector->camelize($property).'Value';
432
433
            if (!method_exists($class, $method)) {
434
                $method = false;
435
            }
436
437
            self::$mutators[$k] = $method;
438
        }
439
440
        return self::$mutators[$k];
441
    }
442
443
    /**
444
     * Gets the accessor method name for a given proeprty name.
445
     * Looks for methods in the form of `getPropertyValue`.
446
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
447
     *
448
     * @param string $property
449
     *
450
     * @return string|false method name if it exists
451
     */
452 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...
453
    {
454
        $class = get_called_class();
455
456
        $k = $class.':'.$property;
457
        if (!array_key_exists($k, self::$accessors)) {
458
            $inflector = Inflector::get();
459
            $method = 'get'.$inflector->camelize($property).'Value';
460
461
            if (!method_exists($class, $method)) {
462
                $method = false;
463
            }
464
465
            self::$accessors[$k] = $method;
466
        }
467
468
        return self::$accessors[$k];
469
    }
470
471
    /**
472
     * Checks if a given property is a relationship.
473
     *
474
     * @param string $property
475
     *
476
     * @return bool
477
     */
478
    public static function isRelationship($property)
479
    {
480
        return in_array($property, static::$relationships);
481
    }
482
483
    /**
484
     * Gets the title of a property.
485
     *
486
     * @param string $name
487
     *
488
     * @return string
489
     */
490
    public static function getPropertyTitle($name)
491
    {
492
        // attmept to fetch the title from the Locale service
493
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
494
        if (self::$locale && $title = self::$locale->t($k)) {
495
            if ($title != $k) {
496
                return $title;
497
            }
498
        }
499
500
        return Inflector::get()->humanize($name);
501
    }
502
503
    /**
504
     * Gets the type cast for a property.
505
     *
506
     * @param string $property
507
     *
508
     * @return string|null
509
     */
510
    public static function getPropertyType($property)
511
    {
512
        if (property_exists(get_called_class(), 'casts')) {
513
            return array_value(static::$casts, $property);
514
        }
515
    }
516
517
    /**
518
     * Casts a value to a given type.
519
     *
520
     * @param string|null $type
521
     * @param mixed       $value
522
     *
523
     * @return mixed casted value
524
     */
525
    public static function cast($type, $value)
526
    {
527
        if ($value === null) {
528
            return;
529
        }
530
531
        switch ($type) {
532
        case self::TYPE_STRING:
533
            return (string) $value;
534
535
        case self::TYPE_INTEGER:
536
            return (int) $value;
537
538
        case self::TYPE_FLOAT:
539
            return (float) $value;
540
541
        case self::TYPE_BOOLEAN:
542
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
543
544
        case self::TYPE_DATE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
545
            // cast dates as unix timestamps
546
            if (!is_numeric($value)) {
547
                return strtotime($value);
548
            } else {
549
                return $value + 0;
550
            }
551
552 View Code Duplication
        case self::TYPE_ARRAY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
553
            // decode JSON into an array
554
            if (is_string($value)) {
555
                return json_decode($value, true);
556
            } else {
557
                return (array) $value;
558
            }
559
560 View Code Duplication
        case self::TYPE_OBJECT:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
561
            // decode JSON into an object
562
            if (is_string($value)) {
563
                return (object) json_decode($value);
564
            } else {
565
                return (object) $value;
566
            }
567
568
        default:
569
            return $value;
570
        }
571
    }
572
573
    /**
574
     * Gets the properties of this model.
575
     *
576
     * @return array
577
     */
578
    public function getProperties()
579
    {
580
        return array_unique(array_merge(
581
            static::$ids, array_keys($this->_values)));
582
    }
583
584
    /**
585
     * Checks if the model has a property.
586
     *
587
     * @param string $property
588
     *
589
     * @return bool has property
590
     */
591
    public function hasProperty($property)
592
    {
593
        return array_key_exists($property, $this->_values) ||
594
               in_array($property, static::$ids);
595
    }
596
597
    /////////////////////////////
598
    // Values
599
    /////////////////////////////
600
601
    /**
602
     * Sets an unsaved value.
603
     *
604
     * @param string $name
605
     * @param mixed  $value
606
     *
607
     * @throws BadMethodCallException when setting a relationship
608
     *
609
     * @return self
610
     */
611
    public function setValue($name, $value)
612
    {
613
        if (static::isRelationship($name)) {
614
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
615
        }
616
617
        // cast the value
618
        if ($type = static::getPropertyType($name)) {
619
            $value = static::cast($type, $value);
620
        }
621
622
        // set using any mutators
623
        if ($mutator = self::getMutator($name)) {
624
            $this->_unsaved[$name] = $this->$mutator($value);
625
        } else {
626
            $this->_unsaved[$name] = $value;
627
        }
628
629
        return $this;
630
    }
631
632
    /**
633
     * Sets a collection values on the model from an untrusted input.
634
     *
635
     * @param array $values
636
     *
637
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
638
     *
639
     * @return self
640
     */
641
    public function setValues($values)
642
    {
643
        // check if the model has a mass assignment whitelist
644
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
645
646
        // if no whitelist, then check for a blacklist
647
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
648
649
        foreach ($values as $k => $value) {
650
            // check for mass assignment violations
651
            if (($permitted && !in_array($k, $permitted)) ||
652
                ($protected && in_array($k, $protected))) {
653
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
654
            }
655
656
            $this->setValue($k, $value);
657
        }
658
659
        return $this;
660
    }
661
662
    /**
663
     * Ignores unsaved values when fetching the next value.
664
     *
665
     * @return self
666
     */
667
    public function ignoreUnsaved()
668
    {
669
        $this->_ignoreUnsaved = true;
670
671
        return $this;
672
    }
673
674
    /**
675
     * Gets property values from the model.
676
     *
677
     * This method looks up values from these locations in this
678
     * precedence order (least important to most important):
679
     *  1. local values
680
     *  2. unsaved values
681
     *
682
     * @param array $properties list of property names to fetch values of
683
     *
684
     * @return array
685
     *
686
     * @throws InvalidArgumentException when a property was requested not present in the values
687
     */
688
    public function get(array $properties)
689
    {
690
        // load the values from the local model cache
691
        $values = $this->_values;
692
693
        // unless specified, use any unsaved values
694
        $ignoreUnsaved = $this->_ignoreUnsaved;
695
        $this->_ignoreUnsaved = false;
696
        if (!$ignoreUnsaved) {
697
            $values = array_replace($values, $this->_unsaved);
698
        }
699
700
        // build the response
701
        $result = [];
702
        foreach ($properties as $k) {
703
            $accessor = self::getAccessor($k);
704
705
            // use the supplied value if it's available
706
            if (array_key_exists($k, $values)) {
707
                $result[$k] = $values[$k];
708
            // get relationship values
709
            } elseif (static::isRelationship($k)) {
710
                $result[$k] = $this->loadRelationship($k);
711
            // set any missing values to null
712
            } elseif ($this->hasProperty($k)) {
713
                $result[$k] = $this->_values[$k] = null;
714
            // throw an exception for non-properties that do not
715
            // have an accessor
716
            } 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...
717
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
718
            // otherwise the value is considered null
719
            } else {
720
                $result[$k] = null;
721
            }
722
723
            // call any accessors
724
            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...
725
                $result[$k] = $this->$accessor($result[$k]);
726
            }
727
        }
728
729
        return $result;
730
    }
731
732
    /**
733
     * Converts the model to an array.
734
     *
735
     * @return array model array
736
     */
737
    public function toArray()
738
    {
739
        // build the list of properties to retrieve
740
        $properties = $this->getProperties();
741
742
        // remove any hidden properties
743
        if (property_exists($this, 'hidden')) {
744
            $properties = array_diff($properties, static::$hidden);
745
        }
746
747
        // include any appended properties
748
        if (property_exists($this, 'appended')) {
749
            $properties = array_unique(array_merge($properties, static::$appended));
750
        }
751
752
        // get the values for the properties
753
        $result = $this->get($properties);
754
755
        // convert any models to arrays
756
        foreach ($result as &$value) {
757
            if ($value instanceof self) {
758
                $value = $value->toArray();
759
            }
760
        }
761
762
        return $result;
763
    }
764
765
    /////////////////////////////
766
    // Persistence
767
    /////////////////////////////
768
769
    /**
770
     * Saves the model.
771
     *
772
     * @return bool
773
     */
774
    public function save()
775
    {
776
        if (!$this->_persisted) {
777
            return $this->create();
778
        }
779
780
        return $this->set();
781
    }
782
783
    /**
784
     * Creates a new model.
785
     *
786
     * @param array $data optional key-value properties to set
787
     *
788
     * @return bool
789
     *
790
     * @throws BadMethodCallException when called on an existing model
791
     */
792
    public function create(array $data = [])
793
    {
794
        if ($this->_persisted) {
795
            throw new BadMethodCallException('Cannot call create() on an existing model');
796
        }
797
798
        // mass assign values passed into create()
799
        $this->setValues($data);
800
801
        // add in any preset values
802
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
803
804
        // dispatch the model.creating event
805
        $event = $this->dispatch(ModelEvent::CREATING);
806
        if ($event->isPropagationStopped()) {
807
            return false;
808
        }
809
810
        // validate the model
811
        if (!$this->valid()) {
812
            return false;
813
        }
814
815
        // create the model using the driver
816
        if (!self::getDriver()->createModel($this, $this->_unsaved)) {
817
            return false;
818
        }
819
820
        // update the model with the persisted values and new ID(s)
821
        $newValues = array_replace(
822
            $this->_unsaved,
823
            $this->getNewIds());
824
        $this->refreshWith($newValues);
825
826
        // dispatch the model.created event
827
        $event = $this->dispatch(ModelEvent::CREATED);
828
829
        return !$event->isPropagationStopped();
830
    }
831
832
    /**
833
     * Gets the IDs for a newly created model.
834
     *
835
     * @return string
836
     */
837
    protected function getNewIds()
838
    {
839
        $ids = [];
840
        foreach (static::$ids as $k) {
841
            // check if the ID property was already given,
842
            if (isset($this->_unsaved[$k])) {
843
                $ids[$k] = $this->_unsaved[$k];
844
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
845
            } else {
846
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
847
            }
848
        }
849
850
        return $ids;
851
    }
852
853
    /**
854
     * Updates the model.
855
     *
856
     * @param array $data optional key-value properties to set
857
     *
858
     * @return bool
859
     *
860
     * @throws BadMethodCallException when not called on an existing model
861
     */
862
    public function set(array $data = [])
863
    {
864
        if (!$this->_persisted) {
865
            throw new BadMethodCallException('Can only call set() on an existing model');
866
        }
867
868
        // mass assign values passed into set()
869
        $this->setValues($data);
870
871
        // not updating anything?
872
        if (count($this->_unsaved) === 0) {
873
            return true;
874
        }
875
876
        // dispatch the model.updating event
877
        $event = $this->dispatch(ModelEvent::UPDATING);
878
        if ($event->isPropagationStopped()) {
879
            return false;
880
        }
881
882
        // validate the model
883
        if (!$this->valid()) {
884
            return false;
885
        }
886
887
        // update the model using the driver
888
        if (!self::getDriver()->updateModel($this, $this->_unsaved)) {
889
            return false;
890
        }
891
892
        // update the model with the persisted values
893
        $this->refreshWith($this->_unsaved);
894
895
        // dispatch the model.updated event
896
        $event = $this->dispatch(ModelEvent::UPDATED);
897
898
        return !$event->isPropagationStopped();
899
    }
900
901
    /**
902
     * Delete the model.
903
     *
904
     * @return bool success
905
     */
906
    public function delete()
907
    {
908
        if (!$this->_persisted) {
909
            throw new BadMethodCallException('Can only call delete() on an existing model');
910
        }
911
912
        // dispatch the model.deleting event
913
        $event = $this->dispatch(ModelEvent::DELETING);
914
        if ($event->isPropagationStopped()) {
915
            return false;
916
        }
917
918
        $deleted = self::getDriver()->deleteModel($this);
919
920
        if ($deleted) {
921
            // dispatch the model.deleted event
922
            $event = $this->dispatch(ModelEvent::DELETED);
923
            if ($event->isPropagationStopped()) {
924
                return false;
925
            }
926
927
            $this->_persisted = false;
928
        }
929
930
        return $deleted;
931
    }
932
933
    /**
934
     * Tells if the model has been persisted.
935
     *
936
     * @return bool
937
     */
938
    public function persisted()
939
    {
940
        return $this->_persisted;
941
    }
942
943
    /**
944
     * Loads the model from the data layer.
945
     *
946
     * @return self
947
     *
948
     * @throws NotFoundException
949
     */
950
    public function refresh()
951
    {
952
        if (!$this->_persisted) {
953
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
954
        }
955
956
        $query = static::query();
957
        $query->where($this->ids());
958
959
        $values = self::getDriver()->queryModels($query);
960
961
        if (count($values) === 0) {
962
            return $this;
963
        }
964
965
        return $this->refreshWith($values[0]);
966
    }
967
968
    /**
969
     * Loads values into the model retrieved from the data layer.
970
     *
971
     * @param array $values values
972
     *
973
     * @return self
974
     */
975
    public function refreshWith(array $values)
976
    {
977
        // cast the values
978
        if (property_exists($this, 'casts')) {
979
            foreach ($values as $k => &$value) {
980
                if ($type = static::getPropertyType($k)) {
981
                    $value = static::cast($type, $value);
982
                }
983
            }
984
        }
985
986
        $this->_persisted = true;
987
        $this->_values = $values;
988
        $this->_unsaved = [];
989
990
        return $this;
991
    }
992
993
    /////////////////////////////
994
    // Queries
995
    /////////////////////////////
996
997
    /**
998
     * Generates a new query instance.
999
     *
1000
     * @return Query
1001
     */
1002
    public static function query()
1003
    {
1004
        // Create a new model instance for the query to ensure
1005
        // that the model's initialize() method gets called.
1006
        // Otherwise, the property definitions will be incomplete.
1007
        $model = new static();
1008
1009
        return new Query($model);
1010
    }
1011
1012
    /**
1013
     * Finds a single instance of a model given it's ID.
1014
     *
1015
     * @param mixed $id
1016
     *
1017
     * @return Model|null
1018
     */
1019
    public static function find($id)
1020
    {
1021
        $model = static::buildFromId($id);
1022
1023
        return static::query()->where($model->ids())->first();
1024
    }
1025
1026
    /**
1027
     * Finds a single instance of a model given it's ID or throws an exception.
1028
     *
1029
     * @param mixed $id
1030
     *
1031
     * @return Model|false
1032
     *
1033
     * @throws NotFoundException when a model could not be found
1034
     */
1035
    public static function findOrFail($id)
1036
    {
1037
        $model = static::find($id);
1038
        if (!$model) {
1039
            throw new NotFoundException('Could not find the requested '.static::modelName());
1040
        }
1041
1042
        return $model;
1043
    }
1044
1045
    /**
1046
     * Gets the toal number of records matching an optional criteria.
1047
     *
1048
     * @param array $where criteria
1049
     *
1050
     * @return int total
1051
     */
1052
    public static function totalRecords(array $where = [])
1053
    {
1054
        $query = static::query();
1055
        $query->where($where);
1056
1057
        return self::getDriver()->totalRecords($query);
1058
    }
1059
1060
    /////////////////////////////
1061
    // Relationships
1062
    /////////////////////////////
1063
1064
    /**
1065
     * Creates the parent side of a One-To-One relationship.
1066
     *
1067
     * @param string $model      foreign model class
1068
     * @param string $foreignKey identifying key on foreign model
1069
     * @param string $localKey   identifying key on local model
1070
     *
1071
     * @return \Pulsar\Relation\Relation
1072
     */
1073 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...
1074
    {
1075
        // the default local key would look like `user_id`
1076
        // for a model named User
1077
        if (!$foreignKey) {
1078
            $inflector = Inflector::get();
1079
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1080
        }
1081
1082
        if (!$localKey) {
1083
            $localKey = self::DEFAULT_ID_PROPERTY;
1084
        }
1085
1086
        return new HasOne($model, $foreignKey, $localKey, $this);
1087
    }
1088
1089
    /**
1090
     * Creates the child side of a One-To-One or One-To-Many relationship.
1091
     *
1092
     * @param string $model      foreign model class
1093
     * @param string $foreignKey identifying key on foreign model
1094
     * @param string $localKey   identifying key on local model
1095
     *
1096
     * @return \Pulsar\Relation\Relation
1097
     */
1098 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...
1099
    {
1100
        if (!$foreignKey) {
1101
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1102
        }
1103
1104
        // the default local key would look like `user_id`
1105
        // for a model named User
1106
        if (!$localKey) {
1107
            $inflector = Inflector::get();
1108
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1109
        }
1110
1111
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1112
    }
1113
1114
    /**
1115
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1116
     *
1117
     * @param string $model      foreign model class
1118
     * @param string $foreignKey identifying key on foreign model
1119
     * @param string $localKey   identifying key on local model
1120
     *
1121
     * @return \Pulsar\Relation\Relation
1122
     */
1123 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...
1124
    {
1125
        // the default local key would look like `user_id`
1126
        // for a model named User
1127
        if (!$foreignKey) {
1128
            $inflector = Inflector::get();
1129
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1130
        }
1131
1132
        if (!$localKey) {
1133
            $localKey = self::DEFAULT_ID_PROPERTY;
1134
        }
1135
1136
        return new HasMany($model, $foreignKey, $localKey, $this);
1137
    }
1138
1139
    /**
1140
     * Creates the child side of a Many-To-Many relationship.
1141
     *
1142
     * @param string $model      foreign model class
1143
     * @param string $foreignKey identifying key on foreign model
1144
     * @param string $localKey   identifying key on local model
1145
     *
1146
     * @return \Pulsar\Relation\Relation
1147
     */
1148 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...
1149
    {
1150
        if (!$foreignKey) {
1151
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1152
        }
1153
1154
        // the default local key would look like `user_id`
1155
        // for a model named User
1156
        if (!$localKey) {
1157
            $inflector = Inflector::get();
1158
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1159
        }
1160
1161
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1162
    }
1163
1164
    /**
1165
     * Loads a given relationship (if not already) and returns
1166
     * its results.
1167
     *
1168
     * @param string $name
1169
     *
1170
     * @return mixed
1171
     */
1172
    protected function loadRelationship($name)
1173
    {
1174
        if (!isset($this->_values[$name])) {
1175
            $relationship = $this->$name();
1176
            $this->_values[$name] = $relationship->getResults();
1177
        }
1178
1179
        return $this->_values[$name];
1180
    }
1181
1182
    /////////////////////////////
1183
    // Events
1184
    /////////////////////////////
1185
1186
    /**
1187
     * Gets the event dispatcher.
1188
     *
1189
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1190
     */
1191
    public static function getDispatcher($ignoreCache = false)
1192
    {
1193
        $class = get_called_class();
1194
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1195
            self::$dispatchers[$class] = new EventDispatcher();
1196
        }
1197
1198
        return self::$dispatchers[$class];
1199
    }
1200
1201
    /**
1202
     * Subscribes to a listener to an event.
1203
     *
1204
     * @param string   $event    event name
1205
     * @param callable $listener
1206
     * @param int      $priority optional priority, higher #s get called first
1207
     */
1208
    public static function listen($event, callable $listener, $priority = 0)
1209
    {
1210
        static::getDispatcher()->addListener($event, $listener, $priority);
1211
    }
1212
1213
    /**
1214
     * Adds a listener to the model.creating event.
1215
     *
1216
     * @param callable $listener
1217
     * @param int      $priority
1218
     */
1219
    public static function creating(callable $listener, $priority = 0)
1220
    {
1221
        static::listen(ModelEvent::CREATING, $listener, $priority);
1222
    }
1223
1224
    /**
1225
     * Adds a listener to the model.created event.
1226
     *
1227
     * @param callable $listener
1228
     * @param int      $priority
1229
     */
1230
    public static function created(callable $listener, $priority = 0)
1231
    {
1232
        static::listen(ModelEvent::CREATED, $listener, $priority);
1233
    }
1234
1235
    /**
1236
     * Adds a listener to the model.updating event.
1237
     *
1238
     * @param callable $listener
1239
     * @param int      $priority
1240
     */
1241
    public static function updating(callable $listener, $priority = 0)
1242
    {
1243
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1244
    }
1245
1246
    /**
1247
     * Adds a listener to the model.updated event.
1248
     *
1249
     * @param callable $listener
1250
     * @param int      $priority
1251
     */
1252
    public static function updated(callable $listener, $priority = 0)
1253
    {
1254
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1255
    }
1256
1257
    /**
1258
     * Adds a listener to the model.deleting event.
1259
     *
1260
     * @param callable $listener
1261
     * @param int      $priority
1262
     */
1263
    public static function deleting(callable $listener, $priority = 0)
1264
    {
1265
        static::listen(ModelEvent::DELETING, $listener, $priority);
1266
    }
1267
1268
    /**
1269
     * Adds a listener to the model.deleted event.
1270
     *
1271
     * @param callable $listener
1272
     * @param int      $priority
1273
     */
1274
    public static function deleted(callable $listener, $priority = 0)
1275
    {
1276
        static::listen(ModelEvent::DELETED, $listener, $priority);
1277
    }
1278
1279
    /**
1280
     * Dispatches an event.
1281
     *
1282
     * @param string $eventName
1283
     *
1284
     * @return ModelEvent
1285
     */
1286
    protected function dispatch($eventName)
1287
    {
1288
        $event = new ModelEvent($this);
1289
1290
        return static::getDispatcher()->dispatch($eventName, $event);
1291
    }
1292
1293
    /////////////////////////////
1294
    // Validation
1295
    /////////////////////////////
1296
1297
    /**
1298
     * Gets the error stack for this model instance. Used to
1299
     * keep track of validation errors.
1300
     *
1301
     * @return Errors
1302
     */
1303
    public function errors()
1304
    {
1305
        if (!$this->_errors) {
1306
            $this->_errors = new Errors($this, self::$locale);
1307
        }
1308
1309
        return $this->_errors;
1310
    }
1311
1312
    /**
1313
     * Checks if the model is valid in its current state.
1314
     *
1315
     * @return bool
1316
     */
1317
    public function valid()
1318
    {
1319
        // clear any previous errors
1320
        $this->errors()->clear();
1321
1322
        // run the validator against the model values
1323
        $validator = $this->getValidator();
1324
        $values = $this->_unsaved + $this->_values;
1325
        $validated = $validator->validate($values);
1326
1327
        // add back any modified unsaved values
1328
        foreach (array_keys($this->_unsaved) as $k) {
1329
            $this->_unsaved[$k] = $values[$k];
1330
        }
1331
1332
        return $validated;
1333
    }
1334
1335
    /**
1336
     * Gets a new validator instance for this model.
1337
     * 
1338
     * @return Validator
1339
     */
1340
    public function getValidator()
1341
    {
1342
        return new Validator(static::$validations, $this->errors());
1343
    }
1344
}
1345