Completed
Push — master ( 18940d...64e92f )
by Jared
02:10
created

Model::getNewID()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 6
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use BadMethodCallException;
15
use ICanBoogie\Inflector;
16
use Pimple\Container;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Exception\DriverMissingException;
19
use Pulsar\Relation\BelongsTo;
20
use Pulsar\Relation\BelongsToMany;
21
use Pulsar\Relation\HasMany;
22
use Pulsar\Relation\HasOne;
23
use Symfony\Component\EventDispatcher\EventDispatcher;
24
25
/**
26
 * Class Model.
27
 */
28
abstract class Model implements \ArrayAccess
29
{
30
    const IMMUTABLE = 0;
31
    const MUTABLE_CREATE_ONLY = 1;
32
    const MUTABLE = 2;
33
34
    const TYPE_STRING = 'string';
35
    const TYPE_NUMBER = 'number';
36
    const TYPE_BOOLEAN = 'boolean';
37
    const TYPE_DATE = 'date';
38
    const TYPE_OBJECT = 'object';
39
    const TYPE_ARRAY = 'array';
40
41
    const ERROR_REQUIRED_FIELD_MISSING = 'required_field_missing';
42
    const ERROR_VALIDATION_FAILED = 'validation_failed';
43
    const ERROR_NOT_UNIQUE = 'not_unique';
44
45
    const DEFAULT_ID_PROPERTY = 'id';
46
47
    /////////////////////////////
48
    // Model visible variables
49
    /////////////////////////////
50
51
    /**
52
     * List of model ID property names.
53
     *
54
     * @var array
55
     */
56
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
57
58
    /**
59
     * Property definitions expressed as a key-value map with
60
     * property names as the keys.
61
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
62
     *
63
     * @var array
64
     */
65
    protected static $properties = [];
66
67
    /**
68
     * @var Container
69
     */
70
    protected static $injectedApp;
71
72
    /**
73
     * @var array
74
     */
75
    protected static $dispatchers;
76
77
    /**
78
     * @var number|string|bool
79
     */
80
    protected $_id;
81
82
    /**
83
     * @var Container
84
     */
85
    protected $app;
86
87
    /**
88
     * @var array
89
     */
90
    protected $_values = [];
91
92
    /**
93
     * @var array
94
     */
95
    protected $_unsaved = [];
96
97
    /**
98
     * @var array
99
     */
100
    protected $_relationships = [];
101
102
    /////////////////////////////
103
    // Base model variables
104
    /////////////////////////////
105
106
    /**
107
     * @var 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
     * @var array
119
     */
120
    private static $defaultIDProperty = [
121
        'type' => self::TYPE_NUMBER,
122
        'mutable' => self::IMMUTABLE,
123
    ];
124
125
    /**
126
     * @var 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
     * @var array
143
     */
144
    private static $initialized = [];
145
146
    /**
147
     * @var DriverInterface
148
     */
149
    private static $driver;
150
151
    /**
152
     * @var array
153
     */
154
    private static $accessors = [];
155
156
    /**
157
     * @var array
158
     */
159
    private static $mutators = [];
160
161
    /**
162
     * @var bool
163
     */
164
    private $_ignoreUnsaved;
165
166
    /**
167
     * Creates a new model object.
168
     *
169
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
170
     * @param array                    $values optional key-value map to pre-seed model
171
     */
172
    public function __construct($id = false, array $values = [])
173
    {
174
        // initialize the model
175
        $this->app = self::$injectedApp;
176
        $this->init();
177
178
        // TODO need to store the id as an array
179
        // instead of a string to maintain type integrity
180
        if (is_array($id)) {
181
            // A model can be supplied as a primary key
182
            foreach ($id as &$el) {
183
                if ($el instanceof self) {
184
                    $el = $el->id();
185
                }
186
            }
187
188
            $id = implode(',', $id);
189
        // A model can be supplied as a primary key
190
        } elseif ($id instanceof self) {
191
            $id = $id->id();
192
        }
193
194
        $this->_id = $id;
195
196
        // load any given values
197
        if (count($values) > 0) {
198
            $this->refreshWith($values);
199
        }
200
    }
201
202
    /**
203
     * Performs initialization on this model.
204
     */
205
    private function init()
206
    {
207
        // ensure the initialize function is called only once
208
        $k = get_called_class();
209
        if (!isset(self::$initialized[$k])) {
210
            $this->initialize();
211
            self::$initialized[$k] = true;
212
        }
213
    }
214
215
    /**
216
     * The initialize() method is called once per model. It's used
217
     * to perform any one-off tasks before the model gets
218
     * constructed. This is a great place to add any model
219
     * properties. When extending this method be sure to call
220
     * parent::initialize() as some important stuff happens here.
221
     * If extending this method to add properties then you should
222
     * call parent::initialize() after adding any properties.
223
     */
224
    protected function initialize()
225
    {
226
        // load the driver
227
        static::getDriver();
228
229
        // add in the default ID property
230
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
231
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
232
        }
233
234
        // add in the auto timestamp properties
235
        if (property_exists(get_called_class(), 'autoTimestamps')) {
236
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
237
        }
238
239
        // fill in each property by extending the property
240
        // definition base
241
        foreach (static::$properties as &$property) {
242
            $property = array_replace(self::$propertyDefinitionBase, $property);
243
        }
244
245
        // order the properties array by name for consistency
246
        // since it is constructed in a random order
247
        ksort(static::$properties);
248
    }
249
250
    /**
251
     * Injects a DI container.
252
     *
253
     * @param Container $app
254
     */
255
    public static function inject(Container $app)
256
    {
257
        self::$injectedApp = $app;
258
    }
259
260
    /**
261
     * Gets the DI container used for this model.
262
     *
263
     * @return Container
264
     */
265
    public function getApp()
266
    {
267
        return $this->app;
268
    }
269
270
    /**
271
     * Sets the driver for all models.
272
     *
273
     * @param DriverInterface $driver
274
     */
275
    public static function setDriver(DriverInterface $driver)
276
    {
277
        self::$driver = $driver;
278
    }
279
280
    /**
281
     * Gets the driver for all models.
282
     *
283
     * @return DriverInterface
284
     *
285
     * @throws DriverMissingException when a driver has not been set yet
286
     */
287
    public static function getDriver()
288
    {
289
        if (!self::$driver) {
290
            throw new DriverMissingException('A model driver has not been set yet.');
291
        }
292
293
        return self::$driver;
294
    }
295
296
    /**
297
     * Clears the driver for all models.
298
     */
299
    public static function clearDriver()
300
    {
301
        self::$driver = null;
302
    }
303
304
    /**
305
     * Gets the name of the model, i.e. User.
306
     *
307
     * @return string
308
     */
309
    public static function modelName()
310
    {
311
        // strip namespacing
312
        $paths = explode('\\', get_called_class());
313
314
        return end($paths);
315
    }
316
317
    /**
318
     * Gets the model ID.
319
     *
320
     * @return string|number|false ID
321
     */
322
    public function id()
323
    {
324
        return $this->_id;
325
    }
326
327
    /**
328
     * Gets a key-value map of the model ID.
329
     *
330
     * @return array ID map
331
     */
332
    public function ids()
333
    {
334
        $return = [];
335
336
        // match up id values from comma-separated id string with property names
337
        $ids = explode(',', $this->_id);
338
        $ids = array_reverse($ids);
339
340
        // TODO need to store the id as an array
341
        // instead of a string to maintain type integrity
342
        foreach (static::$ids as $k => $f) {
343
            $id = (count($ids) > 0) ? array_pop($ids) : false;
344
345
            $return[$f] = $id;
346
        }
347
348
        return $return;
349
    }
350
351
    /////////////////////////////
352
    // Magic Methods
353
    /////////////////////////////
354
355
    /**
356
     * Converts the model into a string.
357
     *
358
     * @return string
359
     */
360
    public function __toString()
361
    {
362
        return get_called_class().'('.$this->_id.')';
363
    }
364
365
    /**
366
     * Shortcut to a get() call for a given property.
367
     *
368
     * @param string $name
369
     *
370
     * @return mixed
371
     */
372
    public function __get($name)
373
    {
374
        $result = $this->get([$name]);
375
376
        return reset($result);
377
    }
378
379
    /**
380
     * Sets an unsaved value.
381
     *
382
     * @param string $name
383
     * @param mixed  $value
384
     */
385
    public function __set($name, $value)
386
    {
387
        // if changing property, remove relation model
388
        if (isset($this->_relationships[$name])) {
389
            unset($this->_relationships[$name]);
390
        }
391
392
        // call any mutators
393
        $mutator = self::getMutator($name);
394
        if ($mutator) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mutator 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...
395
            $this->_unsaved[$name] = $this->$mutator($value);
396
        } else {
397
            $this->_unsaved[$name] = $value;
398
        }
399
    }
400
401
    /**
402
     * Checks if an unsaved value or property exists by this name.
403
     *
404
     * @param string $name
405
     *
406
     * @return bool
407
     */
408
    public function __isset($name)
409
    {
410
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
411
    }
412
413
    /**
414
     * Unsets an unsaved value.
415
     *
416
     * @param string $name
417
     */
418
    public function __unset($name)
419
    {
420
        if (array_key_exists($name, $this->_unsaved)) {
421
            // if changing property, remove relation model
422
            if (isset($this->_relationships[$name])) {
423
                unset($this->_relationships[$name]);
424
            }
425
426
            unset($this->_unsaved[$name]);
427
        }
428
    }
429
430
    /////////////////////////////
431
    // ArrayAccess Interface
432
    /////////////////////////////
433
434
    public function offsetExists($offset)
435
    {
436
        return isset($this->$offset);
437
    }
438
439
    public function offsetGet($offset)
440
    {
441
        return $this->$offset;
442
    }
443
444
    public function offsetSet($offset, $value)
445
    {
446
        $this->$offset = $value;
447
    }
448
449
    public function offsetUnset($offset)
450
    {
451
        unset($this->$offset);
452
    }
453
454
    public static function __callStatic($name, $parameters)
455
    {
456
        // Any calls to unkown static methods should be deferred to
457
        // the query. This allows calls like User::where()
458
        // 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...
459
        return call_user_func_array([static::query(), $name], $parameters);
460
    }
461
462
    /////////////////////////////
463
    // Property Definitions
464
    /////////////////////////////
465
466
    /**
467
     * Gets all the property definitions for the model.
468
     *
469
     * @return array key-value map of properties
470
     */
471
    public static function getProperties()
472
    {
473
        return static::$properties;
474
    }
475
476
    /**
477
     * Gets a property defition for the model.
478
     *
479
     * @param string $property property to lookup
480
     *
481
     * @return array|null property
482
     */
483
    public static function getProperty($property)
484
    {
485
        return array_value(static::$properties, $property);
486
    }
487
488
    /**
489
     * Gets the names of the model ID properties.
490
     *
491
     * @return array
492
     */
493
    public static function getIDProperties()
494
    {
495
        return static::$ids;
496
    }
497
498
    /**
499
     * Checks if the model has a property.
500
     *
501
     * @param string $property property
502
     *
503
     * @return bool has property
504
     */
505
    public static function hasProperty($property)
506
    {
507
        return isset(static::$properties[$property]);
508
    }
509
510
    /**
511
     * Gets the mutator method name for a given proeprty name.
512
     * Looks for methods in the form of `setPropertyValue`.
513
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
514
     *
515
     * @param string $property property
516
     *
517
     * @return string|false method name if it exists
518
     */
519 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...
520
    {
521
        $class = get_called_class();
522
523
        $k = $class.':'.$property;
524
        if (!array_key_exists($k, self::$mutators)) {
525
            $inflector = Inflector::get();
526
            $method = 'set'.$inflector->camelize($property).'Value';
527
528
            if (!method_exists($class, $method)) {
529
                $method = false;
530
            }
531
532
            self::$mutators[$k] = $method;
533
        }
534
535
        return self::$mutators[$k];
536
    }
537
538
    /**
539
     * Gets the accessor method name for a given proeprty name.
540
     * Looks for methods in the form of `getPropertyValue`.
541
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
542
     *
543
     * @param string $property property
544
     *
545
     * @return string|false method name if it exists
546
     */
547 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...
548
    {
549
        $class = get_called_class();
550
551
        $k = $class.':'.$property;
552
        if (!array_key_exists($k, self::$accessors)) {
553
            $inflector = Inflector::get();
554
            $method = 'get'.$inflector->camelize($property).'Value';
555
556
            if (!method_exists($class, $method)) {
557
                $method = false;
558
            }
559
560
            self::$accessors[$k] = $method;
561
        }
562
563
        return self::$accessors[$k];
564
    }
565
566
    /////////////////////////////
567
    // CRUD Operations
568
    /////////////////////////////
569
570
    /**
571
     * Saves the model.
572
     *
573
     * @return bool true when the operation was successful
574
     */
575
    public function save()
576
    {
577
        if ($this->_id === false) {
578
            return $this->create();
579
        }
580
581
        return $this->set($this->_unsaved);
582
    }
583
584
    /**
585
     * Creates a new model.
586
     *
587
     * @param array $data optional key-value properties to set
588
     *
589
     * @return bool true when the operation was successful
590
     *
591
     * @throws BadMethodCallException when called on an existing model
592
     */
593
    public function create(array $data = [])
594
    {
595
        if ($this->_id !== false) {
596
            throw new BadMethodCallException('Cannot call create() on an existing model');
597
        }
598
599
        if (!empty($data)) {
600
            foreach ($data as $k => $value) {
601
                $this->$k = $value;
602
            }
603
        }
604
605
        // dispatch the model.creating event
606
        if (!$this->handleDispatch(ModelEvent::CREATING)) {
607
            return false;
608
        }
609
610
        $requiredProperties = [];
611
        foreach (static::$properties as $name => $property) {
612
            // build a list of the required properties
613
            if ($property['required']) {
614
                $requiredProperties[] = $name;
615
            }
616
617
            // add in default values
618
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
619
                $this->_unsaved[$name] = $property['default'];
620
            }
621
        }
622
623
        // validate the values being saved
624
        $validated = true;
625
        $insertArray = [];
626
        foreach ($this->_unsaved as $name => $value) {
627
            // exclude if value does not map to a property
628
            if (!isset(static::$properties[$name])) {
629
                continue;
630
            }
631
632
            $property = static::$properties[$name];
633
634
            // cannot insert immutable values
635
            // (unless using the default value)
636
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
637
                continue;
638
            }
639
640
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
641
            $insertArray[$name] = $value;
642
        }
643
644
        // check for required fields
645
        foreach ($requiredProperties as $name) {
646
            if (!isset($insertArray[$name])) {
647
                $property = static::$properties[$name];
648
                $this->app['errors']->push([
649
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
650
                    'params' => [
651
                        'field' => $name,
652
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
653
654
                $validated = false;
655
            }
656
        }
657
658
        if (!$validated) {
659
            return false;
660
        }
661
662
        $created = self::$driver->createModel($this, $insertArray);
663
664
        if ($created) {
665
            // determine the model's new ID
666
            $this->_id = $this->getNewID();
667
668
            // NOTE clear the local cache before the model.created
669
            // event so that fetching values forces a reload
670
            // from the storage layer
671
            $this->clearCache();
672
673
            // dispatch the model.created event
674
            if (!$this->handleDispatch(ModelEvent::CREATED)) {
675
                return false;
676
            }
677
        }
678
679
        return $created;
680
    }
681
682
    /**
683
     * Ignores unsaved values when fetching the next value.
684
     *
685
     * @return self
686
     */
687
    public function ignoreUnsaved()
688
    {
689
        $this->_ignoreUnsaved = true;
690
691
        return $this;
692
    }
693
694
    /**
695
     * Fetches property values from the model.
696
     *
697
     * This method looks up values in this order:
698
     * IDs, local cache, unsaved values, storage layer, defaults
699
     *
700
     * @param array $properties list of property names to fetch values of
701
     *
702
     * @return array
703
     */
704
    public function get(array $properties)
705
    {
706
        // load the values from the IDs and local model cache
707
        $values = array_replace($this->ids(), $this->_values);
708
709
        // unless specified, use any unsaved values
710
        $ignoreUnsaved = $this->_ignoreUnsaved;
711
        $this->_ignoreUnsaved = false;
712
713
        if (!$ignoreUnsaved) {
714
            $values = array_replace($values, $this->_unsaved);
715
        }
716
717
        // attempt to load any missing values from the storage layer
718
        $numMissing = count(array_diff($properties, array_keys($values)));
719
        if ($numMissing > 0) {
720
            $this->refresh();
721
            $values = array_replace($values, $this->_values);
722
723
            if (!$ignoreUnsaved) {
724
                $values = array_replace($values, $this->_unsaved);
725
            }
726
        }
727
728
        // build a key-value map of the requested properties
729
        $return = [];
730
        foreach ($properties as $k) {
731
            if (array_key_exists($k, $values)) {
732
                $return[$k] = $values[$k];
733
            // set any missing values to the default value
734
            } elseif (static::hasProperty($k)) {
735
                $return[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
736
            // use null for values of non-properties
737
            } else {
738
                $return[$k] = null;
739
            }
740
741
            // call any accessors
742
            if ($accessor = self::getAccessor($k)) {
743
                $return[$k] = $this->$accessor($return[$k]);
744
            }
745
        }
746
747
        return $return;
748
    }
749
750
    /**
751
     * Gets the ID for a newly created model.
752
     *
753
     * @return string
754
     */
755
    protected function getNewID()
756
    {
757
        $ids = [];
758
        foreach (static::$ids as $k) {
759
            // attempt use the supplied value if the ID property is mutable
760
            $property = static::getProperty($k);
761
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
762
                $ids[] = $this->_unsaved[$k];
763
            } else {
764
                $ids[] = self::$driver->getCreatedID($this, $k);
765
            }
766
        }
767
768
        // TODO need to store the id as an array
769
        // instead of a string to maintain type integrity
770
        return (count($ids) > 1) ? implode(',', $ids) : $ids[0];
771
    }
772
773
    /**
774
     * Converts the model to an array.
775
     *
776
     * @return array
777
     */
778
    public function toArray()
779
    {
780
        // build the list of properties to retrieve
781
        $properties = array_keys(static::$properties);
782
783
        // remove any hidden properties
784
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
785
        $properties = array_diff($properties, $hide);
786
787
        // add any appended properties
788
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
789
        $properties = array_merge($properties, $append);
790
791
        // get the values for the properties
792
        $result = $this->get($properties);
793
794
        // apply the transformation hook
795
        if (method_exists($this, 'toArrayHook')) {
796
            $this->toArrayHook($result, [], [], []);
0 ignored issues
show
Bug introduced by
The method toArrayHook() does not exist on Pulsar\Model. Did you maybe mean toArray()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
797
        }
798
799
        return $result;
800
    }
801
802
    /**
803
     * Updates the model.
804
     *
805
     * @param array $data optional key-value properties to set
806
     *
807
     * @return bool true when the operation was successful
808
     *
809
     * @throws BadMethodCallException when not called on an existing model
810
     */
811
    public function set(array $data = [])
812
    {
813
        if ($this->_id === false) {
814
            throw new BadMethodCallException('Can only call set() on an existing model');
815
        }
816
817
        // not updating anything?
818
        if (count($data) == 0) {
819
            return true;
820
        }
821
822
        // apply mutators
823
        foreach ($data as $k => $value) {
824
            if ($mutator = self::getMutator($k)) {
825
                $data[$k] = $this->$mutator($value);
826
            }
827
        }
828
829
        // dispatch the model.updating event
830
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
831
            return false;
832
        }
833
834
        // DEPRECATED
835
        if (method_exists($this, 'preSetHook') && !$this->preSetHook($data)) {
0 ignored issues
show
Bug introduced by
The method preSetHook() does not seem to exist on object<Pulsar\Model>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
836
            return false;
837
        }
838
839
        // validate the values being saved
840
        $validated = true;
841
        $updateArray = [];
842
        foreach ($data as $name => $value) {
843
            // exclude if value does not map to a property
844
            if (!isset(static::$properties[$name])) {
845
                continue;
846
            }
847
848
            $property = static::$properties[$name];
849
850
            // can only modify mutable properties
851
            if ($property['mutable'] != self::MUTABLE) {
852
                continue;
853
            }
854
855
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
856
            $updateArray[$name] = $value;
857
        }
858
859
        if (!$validated) {
860
            return false;
861
        }
862
863
        $updated = self::$driver->updateModel($this, $updateArray);
864
865
        if ($updated) {
866
            // NOTE clear the local cache before the model.updated
867
            // event so that fetching values forces a reload
868
            // from the storage layer
869
            $this->clearCache();
870
871
            // dispatch the model.updated event
872
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
873
                return false;
874
            }
875
        }
876
877
        return $updated;
878
    }
879
880
    /**
881
     * Delete the model.
882
     *
883
     * @return bool true when the operation was successful
884
     */
885
    public function delete()
886
    {
887
        if ($this->_id === false) {
888
            throw new BadMethodCallException('Can only call delete() on an existing model');
889
        }
890
891
        // dispatch the model.deleting event
892
        if (!$this->handleDispatch(ModelEvent::DELETING)) {
893
            return false;
894
        }
895
896
        $deleted = self::$driver->deleteModel($this);
897
898
        if ($deleted) {
899
            // dispatch the model.deleted event
900
            if (!$this->handleDispatch(ModelEvent::DELETED)) {
901
                return false;
902
            }
903
904
            // NOTE clear the local cache before the model.deleted
905
            // event so that fetching values forces a reload
906
            // from the storage layer
907
            $this->clearCache();
908
        }
909
910
        return $deleted;
911
    }
912
913
    /////////////////////////////
914
    // Queries
915
    /////////////////////////////
916
917
    /**
918
     * Generates a new query instance.
919
     *
920
     * @return Query
921
     */
922
    public static function query()
923
    {
924
        // Create a new model instance for the query to ensure
925
        // that the model's initialize() method gets called.
926
        // Otherwise, the property definitions will be incomplete.
927
        $model = new static();
928
929
        return new Query($model);
930
    }
931
932
    /**
933
     * Gets the total number of records matching an optional criteria.
934
     *
935
     * @param array $where criteria
936
     *
937
     * @return int
938
     */
939
    public static function totalRecords(array $where = [])
940
    {
941
        $query = static::query();
942
        $query->where($where);
943
944
        return self::getDriver()->totalRecords($query);
945
    }
946
947
    /**
948
     * @deprecated
949
     *
950
     * Checks if the model exists in the database
951
     *
952
     * @return bool
953
     */
954
    public function exists()
955
    {
956
        return static::totalRecords($this->ids()) == 1;
957
    }
958
959
    /**
960
     * Loads the model from the storage layer.
961
     *
962
     * @return self
963
     */
964
    public function refresh()
965
    {
966
        if ($this->_id === false) {
967
            return $this;
968
        }
969
970
        $values = self::$driver->loadModel($this);
971
972
        if (!is_array($values)) {
973
            return $this;
974
        }
975
976
        // clear any relations
977
        $this->_relationships = [];
978
979
        return $this->refreshWith($values);
980
    }
981
982
    /**
983
     * Loads values into the model.
984
     *
985
     * @param array $values values
986
     *
987
     * @return self
988
     */
989
    public function refreshWith(array $values)
990
    {
991
        $this->_values = $values;
992
993
        return $this;
994
    }
995
996
    /**
997
     * Clears the cache for this model.
998
     *
999
     * @return self
1000
     */
1001
    public function clearCache()
1002
    {
1003
        $this->_unsaved = [];
1004
        $this->_values = [];
1005
        $this->_relationships = [];
1006
1007
        return $this;
1008
    }
1009
1010
    /////////////////////////////
1011
    // Relationships
1012
    /////////////////////////////
1013
1014
    /**
1015
     * @deprecated
1016
     *
1017
     * Gets the model object corresponding to a relation
1018
     * WARNING no check is used to see if the model returned actually exists
1019
     *
1020
     * @param string $propertyName property
1021
     *
1022
     * @return Model
1023
     */
1024
    public function relation($propertyName)
1025
    {
1026
        $property = static::getProperty($propertyName);
1027
1028
        if (!isset($this->_relationships[$propertyName])) {
1029
            $relationModelName = $property['relation'];
1030
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1031
        }
1032
1033
        return $this->_relationships[$propertyName];
1034
    }
1035
1036
    /**
1037
     * Creates the parent side of a One-To-One relationship.
1038
     *
1039
     * @param string $model      foreign model class
1040
     * @param string $foreignKey identifying key on foreign model
1041
     * @param string $localKey   identifying key on local model
1042
     *
1043
     * @return Relation\Relation
1044
     */
1045 View Code Duplication
    public function hasOne($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1046
    {
1047
        // the default local key would look like `user_id`
1048
        // for a model named User
1049
        if (!$foreignKey) {
1050
            $inflector = Inflector::get();
1051
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1052
        }
1053
1054
        if (!$localKey) {
1055
            $localKey = self::DEFAULT_ID_PROPERTY;
1056
        }
1057
1058
        return new HasOne($model, $foreignKey, $localKey, $this);
1059
    }
1060
1061
    /**
1062
     * Creates the child side of a One-To-One or One-To-Many relationship.
1063
     *
1064
     * @param string $model      foreign model class
1065
     * @param string $foreignKey identifying key on foreign model
1066
     * @param string $localKey   identifying key on local model
1067
     *
1068
     * @return Relation\Relation
1069
     */
1070 View Code Duplication
    public function belongsTo($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1071
    {
1072
        if (!$foreignKey) {
1073
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1074
        }
1075
1076
        // the default local key would look like `user_id`
1077
        // for a model named User
1078
        if (!$localKey) {
1079
            $inflector = Inflector::get();
1080
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1081
        }
1082
1083
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1084
    }
1085
1086
    /**
1087
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1088
     *
1089
     * @param string $model      foreign model class
1090
     * @param string $foreignKey identifying key on foreign model
1091
     * @param string $localKey   identifying key on local model
1092
     *
1093
     * @return Relation\Relation
1094
     */
1095 View Code Duplication
    public function hasMany($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1096
    {
1097
        // the default local key would look like `user_id`
1098
        // for a model named User
1099
        if (!$foreignKey) {
1100
            $inflector = Inflector::get();
1101
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1102
        }
1103
1104
        if (!$localKey) {
1105
            $localKey = self::DEFAULT_ID_PROPERTY;
1106
        }
1107
1108
        return new HasMany($model, $foreignKey, $localKey, $this);
1109
    }
1110
1111
    /**
1112
     * Creates the child side of a Many-To-Many relationship.
1113
     *
1114
     * @param string $model      foreign model class
1115
     * @param string $foreignKey identifying key on foreign model
1116
     * @param string $localKey   identifying key on local model
1117
     *
1118
     * @return Relation\Relation
1119
     */
1120 View Code Duplication
    public function belongsToMany($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1121
    {
1122
        if (!$foreignKey) {
1123
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1124
        }
1125
1126
        // the default local key would look like `user_id`
1127
        // for a model named User
1128
        if (!$localKey) {
1129
            $inflector = Inflector::get();
1130
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1131
        }
1132
1133
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1134
    }
1135
1136
    /////////////////////////////
1137
    // Events
1138
    /////////////////////////////
1139
1140
    /**
1141
     * Gets the event dispatcher.
1142
     *
1143
     * @return EventDispatcher
1144
     */
1145
    public static function getDispatcher($ignoreCache = false)
1146
    {
1147
        $class = get_called_class();
1148
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1149
            self::$dispatchers[$class] = new EventDispatcher();
1150
        }
1151
1152
        return self::$dispatchers[$class];
1153
    }
1154
1155
    /**
1156
     * Subscribes to a listener to an event.
1157
     *
1158
     * @param string   $event    event name
1159
     * @param callable $listener
1160
     * @param int      $priority optional priority, higher #s get called first
1161
     */
1162
    public static function listen($event, callable $listener, $priority = 0)
1163
    {
1164
        static::getDispatcher()->addListener($event, $listener, $priority);
1165
    }
1166
1167
    /**
1168
     * Adds a listener to the model.creating and model.updating events.
1169
     *
1170
     * @param callable $listener
1171
     * @param int      $priority
1172
     */
1173
    public static function saving(callable $listener, $priority = 0)
1174
    {
1175
        static::listen(ModelEvent::CREATING, $listener, $priority);
1176
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1177
    }
1178
1179
    /**
1180
     * Adds a listener to the model.created and model.updated events.
1181
     *
1182
     * @param callable $listener
1183
     * @param int      $priority
1184
     */
1185
    public static function saved(callable $listener, $priority = 0)
1186
    {
1187
        static::listen(ModelEvent::CREATED, $listener, $priority);
1188
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1189
    }
1190
1191
    /**
1192
     * Adds a listener to the model.creating event.
1193
     *
1194
     * @param callable $listener
1195
     * @param int      $priority
1196
     */
1197
    public static function creating(callable $listener, $priority = 0)
1198
    {
1199
        static::listen(ModelEvent::CREATING, $listener, $priority);
1200
    }
1201
1202
    /**
1203
     * Adds a listener to the model.created event.
1204
     *
1205
     * @param callable $listener
1206
     * @param int      $priority
1207
     */
1208
    public static function created(callable $listener, $priority = 0)
1209
    {
1210
        static::listen(ModelEvent::CREATED, $listener, $priority);
1211
    }
1212
1213
    /**
1214
     * Adds a listener to the model.updating event.
1215
     *
1216
     * @param callable $listener
1217
     * @param int      $priority
1218
     */
1219
    public static function updating(callable $listener, $priority = 0)
1220
    {
1221
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1222
    }
1223
1224
    /**
1225
     * Adds a listener to the model.updated event.
1226
     *
1227
     * @param callable $listener
1228
     * @param int      $priority
1229
     */
1230
    public static function updated(callable $listener, $priority = 0)
1231
    {
1232
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1233
    }
1234
1235
    /**
1236
     * Adds a listener to the model.deleting event.
1237
     *
1238
     * @param callable $listener
1239
     * @param int      $priority
1240
     */
1241
    public static function deleting(callable $listener, $priority = 0)
1242
    {
1243
        static::listen(ModelEvent::DELETING, $listener, $priority);
1244
    }
1245
1246
    /**
1247
     * Adds a listener to the model.deleted event.
1248
     *
1249
     * @param callable $listener
1250
     * @param int      $priority
1251
     */
1252
    public static function deleted(callable $listener, $priority = 0)
1253
    {
1254
        static::listen(ModelEvent::DELETED, $listener, $priority);
1255
    }
1256
1257
    /**
1258
     * Dispatches an event.
1259
     *
1260
     * @param string $eventName
1261
     *
1262
     * @return ModelEvent
1263
     */
1264
    protected function dispatch($eventName)
1265
    {
1266
        $event = new ModelEvent($this);
1267
1268
        return static::getDispatcher()->dispatch($eventName, $event);
1269
    }
1270
1271
    /**
1272
     * Dispatches the given event and checks if it was successful.
1273
     *
1274
     * @param string $eventName
1275
     *
1276
     * @return bool true if the events were successfully propagated
1277
     */
1278
    private function handleDispatch($eventName)
1279
    {
1280
        $event = $this->dispatch($eventName);
1281
1282
        return !$event->isPropagationStopped();
1283
    }
1284
1285
    /////////////////////////////
1286
    // Validation
1287
    /////////////////////////////
1288
1289
    /**
1290
     * Validates and marshals a value to storage.
1291
     *
1292
     * @param array  $property
1293
     * @param string $propertyName
1294
     * @param mixed  $value
1295
     *
1296
     * @return bool
1297
     */
1298
    private function filterAndValidate(array $property, $propertyName, &$value)
1299
    {
1300
        // assume empty string is a null value for properties
1301
        // that are marked as optionally-null
1302
        if ($property['null'] && empty($value)) {
1303
            $value = null;
1304
1305
            return true;
1306
        }
1307
1308
        // validate
1309
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1310
1311
        // unique?
1312
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1313
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1314
        }
1315
1316
        return $valid;
1317
    }
1318
1319
    /**
1320
     * Validates a value for a property.
1321
     *
1322
     * @param array  $property
1323
     * @param string $propertyName
1324
     * @param mixed  $value
1325
     *
1326
     * @return array
1327
     */
1328
    private function validate(array $property, $propertyName, $value)
1329
    {
1330
        $valid = true;
1331
1332
        if (isset($property['validate']) && is_callable($property['validate'])) {
1333
            $valid = call_user_func_array($property['validate'], [$value]);
1334
        } elseif (isset($property['validate'])) {
1335
            $valid = Validate::is($value, $property['validate']);
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Validate::is() has been deprecated with message: Validates one or more fields based upon certain filters. Filters may be chained and will be executed in order
i.e. Validate::is( '[email protected]', 'email' ) or Validate::is( ['password1', 'password2'], 'matching|password:8|required' ). NOTE: some filters may modify the data, which is passed in by reference

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1336
        }
1337
1338
        if (!$valid) {
1339
            $this->app['errors']->push([
1340
                'error' => self::ERROR_VALIDATION_FAILED,
1341
                'params' => [
1342
                    'field' => $propertyName,
1343
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1344
        }
1345
1346
        return [$valid, $value];
1347
    }
1348
1349
    /**
1350
     * Checks if a value is unique for a property.
1351
     *
1352
     * @param array  $property
1353
     * @param string $propertyName
1354
     * @param mixed  $value
1355
     *
1356
     * @return bool
1357
     */
1358
    private function checkUniqueness(array $property, $propertyName, $value)
1359
    {
1360
        if (static::totalRecords([$propertyName => $value]) > 0) {
1361
            $this->app['errors']->push([
1362
                'error' => self::ERROR_NOT_UNIQUE,
1363
                'params' => [
1364
                    'field' => $propertyName,
1365
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1366
1367
            return false;
1368
        }
1369
1370
        return true;
1371
    }
1372
1373
    /**
1374
     * Gets the marshaled default value for a property (if set).
1375
     *
1376
     * @param string $property
1377
     *
1378
     * @return mixed
1379
     */
1380
    private function getPropertyDefault(array $property)
1381
    {
1382
        return array_value($property, 'default');
1383
    }
1384
}
1385