Completed
Push — master ( f96361...9355d7 )
by Jared
02:34
created

Model::getIdProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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