Completed
Push — master ( dadd01...aa571b )
by Jared
02:29
created

Model::updated()   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 2
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 \Pimple\Container
66
     */
67
    protected static $injectedApp;
68
69
    /**
70
     * @staticvar array
71
     */
72
    protected static $dispatchers;
73
74
    /**
75
     * @var number|string|bool
76
     */
77
    protected $_id;
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 array
96
     */
97
    protected $_relationships = [];
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|string|Model|false $id     ordered array of ids or comma-separated id string
172
     * @param array                    $values optional key-value map to pre-seed model
173
     */
174
    public function __construct($id = false, array $values = [])
175
    {
176
        // initialize the model
177
        $this->app = self::$injectedApp;
178
        $this->init();
179
180
        // TODO need to store the id as an array
181
        // instead of a string to maintain type integrity
182
        if (is_array($id)) {
183
            // A model can be supplied as a primary key
184
            foreach ($id as &$el) {
185
                if ($el instanceof self) {
186
                    $el = $el->id();
187
                }
188
            }
189
190
            $id = implode(',', $id);
191
        // A model can be supplied as a primary key
192
        } elseif ($id instanceof self) {
193
            $id = $id->id();
194
        }
195
196
        $this->_id = $id;
197
198
        // load any given values
199
        if (count($values) > 0) {
200
            $this->refreshWith($values);
201
        }
202
    }
203
204
    /**
205
     * Performs initialization on this model.
206
     */
207
    private function init()
208
    {
209
        // ensure the initialize function is called only once
210
        $k = get_called_class();
211
        if (!isset(self::$initialized[$k])) {
212
            $this->initialize();
213
            self::$initialized[$k] = true;
214
        }
215
    }
216
217
    /**
218
     * The initialize() method is called once per model. It's used
219
     * to perform any one-off tasks before the model gets
220
     * constructed. This is a great place to add any model
221
     * properties. When extending this method be sure to call
222
     * parent::initialize() as some important stuff happens here.
223
     * If extending this method to add properties then you should
224
     * call parent::initialize() after adding any properties.
225
     */
226
    protected function initialize()
227
    {
228
        // load the driver
229
        static::getDriver();
0 ignored issues
show
Unused Code introduced by
The call to the method Pulsar\Model::getDriver() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

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