Completed
Push — master ( dddc7a...dadd01 )
by Jared
02:58
created

Model::hasRequiredValues()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 8.8571
cc 5
eloc 12
nc 3
nop 1
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
namespace Pulsar;
12
13
use BadMethodCallException;
14
use ICanBoogie\Inflector;
15
use Infuse\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
        if (!$this->performDispatch(ModelEvent::CREATING)) {
597
            return false;
598
        }
599
600
        foreach (static::$properties as $name => $property) {
601
            // add in default values
602
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
603
                $this->_unsaved[$name] = $property['default'];
604
            }
605
        }
606
607
        // validate the values being saved
608
        $validated = true;
609
        $insertArray = [];
610
        foreach ($this->_unsaved as $name => $value) {
611
            // exclude if value does not map to a property
612
            if (!isset(static::$properties[$name])) {
613
                continue;
614
            }
615
616
            $property = static::$properties[$name];
617
618
            // cannot insert immutable values
619
            // (unless using the default value)
620
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
621
                continue;
622
            }
623
624
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
625
            $insertArray[$name] = $value;
626
        }
627
628
        // the final validation check is to look for required fields
629
        // it should be ran before returning (even if the validation
630
        // has already failed) in order to build a complete list of
631
        // validation errors
632
        if (!$this->hasRequiredValues($insertArray) || !$validated) {
633
            return false;
634
        }
635
636
        $created = self::$driver->createModel($this, $insertArray);
637
638
        if ($created) {
639
            // determine the model's new ID
640
            $this->_id = $this->getNewID();
641
642
            // NOTE clear the local cache before the model.created
643
            // event so that fetching values forces a reload
644
            // from the storage layer
645
            $this->clearCache();
646
647
            // dispatch the model.created event
648
            if (!$this->performDispatch(ModelEvent::CREATED)) {
649
                return false;
650
            }
651
        }
652
653
        return $created;
654
    }
655
656
    /**
657
     * Ignores unsaved values when fetching the next value.
658
     *
659
     * @return self
660
     */
661
    public function ignoreUnsaved()
662
    {
663
        $this->_ignoreUnsaved = true;
664
665
        return $this;
666
    }
667
668
    /**
669
     * Fetches property values from the model.
670
     *
671
     * This method looks up values in this order:
672
     * IDs, local cache, unsaved values, storage layer, defaults
673
     *
674
     * @param array $properties list of property names to fetch values of
675
     *
676
     * @return array
677
     */
678
    public function get(array $properties)
679
    {
680
        // load the values from the IDs and local model cache
681
        $values = array_replace($this->ids(), $this->_values);
682
683
        // unless specified, use any unsaved values
684
        $ignoreUnsaved = $this->_ignoreUnsaved;
685
        $this->_ignoreUnsaved = false;
686
687
        if (!$ignoreUnsaved) {
688
            $values = array_replace($values, $this->_unsaved);
689
        }
690
691
        // attempt to load any missing values from the storage layer
692
        $missing = array_diff($properties, array_keys($values));
693
        if (count($missing) > 0) {
694
            // load values for the model
695
            $this->refresh();
696
            $values = array_replace($values, $this->_values);
697
698
            // add back any unsaved values
699
            if (!$ignoreUnsaved) {
700
                $values = array_replace($values, $this->_unsaved);
701
            }
702
        }
703
704
        return $this->buildGetResponse($properties, $values);
705
    }
706
707
    /**
708
     * Builds a key-value map of the requested properties given a set of values.
709
     *
710
     * @param array $properties
711
     * @param array $values
712
     *
713
     * @return array
714
     *
715
     * @throws InvalidArgumentException when a property was requested not present in the values
716
     */
717
    private function buildGetResponse(array $properties, array $values)
718
    {
719
        $response = [];
720
        foreach ($properties as $k) {
721
            $accessor = self::getAccessor($k);
722
723
            // use the supplied value if it's available
724
            if (array_key_exists($k, $values)) {
725
                $response[$k] = $values[$k];
726
            // set any missing values to the default value
727
            } elseif (static::hasProperty($k)) {
728
                $response[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
729
            // throw an exception for non-properties that do not
730
            // have an accessor
731
            } 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...
732
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
733
            // otherwise the value is considered null
734
            } else {
735
                $response[$k] = null;
736
            }
737
738
            // call any accessors
739
            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...
740
                $response[$k] = $this->$accessor($response[$k]);
741
            }
742
        }
743
744
        return $response;
745
    }
746
747
    /**
748
     * Gets the ID for a newly created model.
749
     *
750
     * @return string
751
     */
752
    protected function getNewID()
753
    {
754
        $ids = [];
755
        foreach (static::$ids as $k) {
756
            // attempt use the supplied value if the ID property is mutable
757
            $property = static::getProperty($k);
758
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
759
                $ids[] = $this->_unsaved[$k];
760
            } else {
761
                $ids[] = self::$driver->getCreatedID($this, $k);
762
            }
763
        }
764
765
        // TODO need to store the id as an array
766
        // instead of a string to maintain type integrity
767
        return (count($ids) > 1) ? implode(',', $ids) : $ids[0];
768
    }
769
770
    /**
771
     * Converts the model to an array.
772
     *
773
     * @return array model array
774
     */
775
    public function toArray()
776
    {
777
        // build the list of properties to retrieve
778
        $properties = array_keys(static::$properties);
779
780
        // remove any hidden properties
781
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
782
        $properties = array_diff($properties, $hide);
783
784
        // add any appended properties
785
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
786
        $properties = array_merge($properties, $append);
787
788
        // get the values for the properties
789
        $result = $this->get($properties);
790
791
        return $result;
792
    }
793
794
    /**
795
     * Updates the model.
796
     *
797
     * @param array $data optional key-value properties to set
798
     *
799
     * @return bool
800
     *
801
     * @throws BadMethodCallException when not called on an existing model
802
     */
803
    public function set(array $data = [])
804
    {
805
        if ($this->_id === false) {
806
            throw new BadMethodCallException('Can only call set() on an existing model');
807
        }
808
809
        // not updating anything?
810
        if (count($data) == 0) {
811
            return true;
812
        }
813
814
        // apply mutators
815
        foreach ($data as $k => $value) {
816
            if ($mutator = self::getMutator($k)) {
817
                $data[$k] = $this->$mutator($value);
818
            }
819
        }
820
821
        // dispatch the model.updating event
822
        if (!$this->performDispatch(ModelEvent::UPDATING)) {
823
            return false;
824
        }
825
826
        // validate the values being saved
827
        $validated = true;
828
        $updateArray = [];
829
        foreach ($data as $name => $value) {
830
            // exclude if value does not map to a property
831
            if (!isset(static::$properties[$name])) {
832
                continue;
833
            }
834
835
            $property = static::$properties[$name];
836
837
            // can only modify mutable properties
838
            if ($property['mutable'] != self::MUTABLE) {
839
                continue;
840
            }
841
842
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
843
            $updateArray[$name] = $value;
844
        }
845
846
        if (!$validated) {
847
            return false;
848
        }
849
850
        $updated = self::$driver->updateModel($this, $updateArray);
851
852
        if ($updated) {
853
            // NOTE clear the local cache before the model.updated
854
            // event so that fetching values forces a reload
855
            // from the storage layer
856
            $this->clearCache();
857
858
            // dispatch the model.updated event
859
            if (!$this->performDispatch(ModelEvent::UPDATED)) {
860
                return false;
861
            }
862
        }
863
864
        return $updated;
865
    }
866
867
    /**
868
     * Delete the model.
869
     *
870
     * @return bool success
871
     */
872
    public function delete()
873
    {
874
        if ($this->_id === false) {
875
            throw new BadMethodCallException('Can only call delete() on an existing model');
876
        }
877
878
        // dispatch the model.deleting event
879
        if (!$this->performDispatch(ModelEvent::DELETING)) {
880
            return false;
881
        }
882
883
        $deleted = self::$driver->deleteModel($this);
884
885
        if ($deleted) {
886
            // dispatch the model.deleted event
887
            if (!$this->performDispatch(ModelEvent::DELETED)) {
888
                return false;
889
            }
890
891
            // NOTE clear the local cache before the model.deleted
892
            // event so that fetching values forces a reload
893
            // from the storage layer
894
            $this->clearCache();
895
        }
896
897
        return $deleted;
898
    }
899
900
    /////////////////////////////
901
    // Queries
902
    /////////////////////////////
903
904
    /**
905
     * Generates a new query instance.
906
     *
907
     * @return Model\Query
908
     */
909
    public static function query()
910
    {
911
        // Create a new model instance for the query to ensure
912
        // that the model's initialize() method gets called.
913
        // Otherwise, the property definitions will be incomplete.
914
        $model = new static();
915
916
        return new Query($model);
917
    }
918
919
    /**
920
     * Gets the toal number of records matching an optional criteria.
921
     *
922
     * @param array $where criteria
923
     *
924
     * @return int total
925
     */
926
    public static function totalRecords(array $where = [])
927
    {
928
        $query = static::query();
929
        $query->where($where);
930
931
        return self::getDriver()->totalRecords($query);
932
    }
933
934
    /**
935
     * Checks if the model exists in the database.
936
     *
937
     * @return bool
938
     */
939
    public function exists()
940
    {
941
        return static::totalRecords($this->ids()) == 1;
942
    }
943
944
    /**
945
     * @deprecated alias for refresh()
946
     */
947
    public function load()
948
    {
949
        return $this->refresh();
950
    }
951
952
    /**
953
     * Loads the model from the storage layer.
954
     *
955
     * @return self
956
     */
957
    public function refresh()
958
    {
959
        if ($this->_id === false) {
960
            return $this;
961
        }
962
963
        $values = self::$driver->loadModel($this);
964
965
        if (!is_array($values)) {
966
            return $this;
967
        }
968
969
        // clear any relations
970
        $this->_relationships = [];
971
972
        return $this->refreshWith($values);
973
    }
974
975
    /**
976
     * Loads values into the model.
977
     *
978
     * @param array $values values
979
     *
980
     * @return self
981
     */
982
    public function refreshWith(array $values)
983
    {
984
        $this->_values = $values;
985
986
        return $this;
987
    }
988
989
    /**
990
     * Clears the cache for this model.
991
     *
992
     * @return self
993
     */
994
    public function clearCache()
995
    {
996
        $this->_unsaved = [];
997
        $this->_values = [];
998
        $this->_relationships = [];
999
1000
        return $this;
1001
    }
1002
1003
    /////////////////////////////
1004
    // Relationships
1005
    /////////////////////////////
1006
1007
    /**
1008
     * Gets the model object corresponding to a relation
1009
     * WARNING no check is used to see if the model returned actually exists.
1010
     *
1011
     * @param string $propertyName property
1012
     *
1013
     * @return \Pulsar\Model model
1014
     */
1015
    public function relation($propertyName)
1016
    {
1017
        // TODO deprecated
1018
        $property = static::getProperty($propertyName);
1019
1020
        if (!isset($this->_relationships[$propertyName])) {
1021
            $relationModelName = $property['relation'];
1022
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1023
        }
1024
1025
        return $this->_relationships[$propertyName];
1026
    }
1027
1028
    /**
1029
     * Creates the parent side of a One-To-One relationship.
1030
     *
1031
     * @param string $model      foreign model class
1032
     * @param string $foreignKey identifying key on foreign model
1033
     * @param string $localKey   identifying key on local model
1034
     *
1035
     * @return Relation
1036
     */
1037 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...
1038
    {
1039
        // the default local key would look like `user_id`
1040
        // for a model named User
1041
        if (!$foreignKey) {
1042
            $inflector = Inflector::get();
1043
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1044
        }
1045
1046
        if (!$localKey) {
1047
            $localKey = self::DEFAULT_ID_PROPERTY;
1048
        }
1049
1050
        return new HasOne($model, $foreignKey, $localKey, $this);
1051
    }
1052
1053
    /**
1054
     * Creates the child side of a One-To-One or One-To-Many relationship.
1055
     *
1056
     * @param string $model      foreign model class
1057
     * @param string $foreignKey identifying key on foreign model
1058
     * @param string $localKey   identifying key on local model
1059
     *
1060
     * @return Relation
1061
     */
1062 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...
1063
    {
1064
        if (!$foreignKey) {
1065
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1066
        }
1067
1068
        // the default local key would look like `user_id`
1069
        // for a model named User
1070
        if (!$localKey) {
1071
            $inflector = Inflector::get();
1072
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1073
        }
1074
1075
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1076
    }
1077
1078
    /**
1079
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1080
     *
1081
     * @param string $model      foreign model class
1082
     * @param string $foreignKey identifying key on foreign model
1083
     * @param string $localKey   identifying key on local model
1084
     *
1085
     * @return Relation
1086
     */
1087 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...
1088
    {
1089
        // the default local key would look like `user_id`
1090
        // for a model named User
1091
        if (!$foreignKey) {
1092
            $inflector = Inflector::get();
1093
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1094
        }
1095
1096
        if (!$localKey) {
1097
            $localKey = self::DEFAULT_ID_PROPERTY;
1098
        }
1099
1100
        return new HasMany($model, $foreignKey, $localKey, $this);
1101
    }
1102
1103
    /**
1104
     * Creates the child side of a Many-To-Many relationship.
1105
     *
1106
     * @param string $model      foreign model class
1107
     * @param string $foreignKey identifying key on foreign model
1108
     * @param string $localKey   identifying key on local model
1109
     *
1110
     * @return Relation
1111
     */
1112 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...
1113
    {
1114
        if (!$foreignKey) {
1115
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1116
        }
1117
1118
        // the default local key would look like `user_id`
1119
        // for a model named User
1120
        if (!$localKey) {
1121
            $inflector = Inflector::get();
1122
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1123
        }
1124
1125
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1126
    }
1127
1128
    /////////////////////////////
1129
    // Events
1130
    /////////////////////////////
1131
1132
    /**
1133
     * Gets the event dispatcher.
1134
     *
1135
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1136
     */
1137
    public static function getDispatcher($ignoreCache = false)
1138
    {
1139
        $class = get_called_class();
1140
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1141
            self::$dispatchers[$class] = new EventDispatcher();
1142
        }
1143
1144
        return self::$dispatchers[$class];
1145
    }
1146
1147
    /**
1148
     * Subscribes to a listener to an event.
1149
     *
1150
     * @param string   $event    event name
1151
     * @param callable $listener
1152
     * @param int      $priority optional priority, higher #s get called first
1153
     */
1154
    public static function listen($event, callable $listener, $priority = 0)
1155
    {
1156
        static::getDispatcher()->addListener($event, $listener, $priority);
1157
    }
1158
1159
    /**
1160
     * Adds a listener to the model.creating event.
1161
     *
1162
     * @param callable $listener
1163
     * @param int      $priority
1164
     */
1165
    public static function creating(callable $listener, $priority = 0)
1166
    {
1167
        static::listen(ModelEvent::CREATING, $listener, $priority);
1168
    }
1169
1170
    /**
1171
     * Adds a listener to the model.created event.
1172
     *
1173
     * @param callable $listener
1174
     * @param int      $priority
1175
     */
1176
    public static function created(callable $listener, $priority = 0)
1177
    {
1178
        static::listen(ModelEvent::CREATED, $listener, $priority);
1179
    }
1180
1181
    /**
1182
     * Adds a listener to the model.updating event.
1183
     *
1184
     * @param callable $listener
1185
     * @param int      $priority
1186
     */
1187
    public static function updating(callable $listener, $priority = 0)
1188
    {
1189
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1190
    }
1191
1192
    /**
1193
     * Adds a listener to the model.updated event.
1194
     *
1195
     * @param callable $listener
1196
     * @param int      $priority
1197
     */
1198
    public static function updated(callable $listener, $priority = 0)
1199
    {
1200
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1201
    }
1202
1203
    /**
1204
     * Adds a listener to the model.deleting event.
1205
     *
1206
     * @param callable $listener
1207
     * @param int      $priority
1208
     */
1209
    public static function deleting(callable $listener, $priority = 0)
1210
    {
1211
        static::listen(ModelEvent::DELETING, $listener, $priority);
1212
    }
1213
1214
    /**
1215
     * Adds a listener to the model.deleted event.
1216
     *
1217
     * @param callable $listener
1218
     * @param int      $priority
1219
     */
1220
    public static function deleted(callable $listener, $priority = 0)
1221
    {
1222
        static::listen(ModelEvent::DELETED, $listener, $priority);
1223
    }
1224
1225
    /**
1226
     * Dispatches an event.
1227
     *
1228
     * @param string $eventName
1229
     *
1230
     * @return Model\ModelEvent
1231
     */
1232
    protected function dispatch($eventName)
1233
    {
1234
        $event = new ModelEvent($this);
1235
1236
        return static::getDispatcher()->dispatch($eventName, $event);
1237
    }
1238
1239
    /**
1240
     * Dispatches the given event and checks if it was successful.
1241
     *
1242
     * @param string $eventName
1243
     *
1244
     * @return bool
1245
     */
1246
    private function performDispatch($eventName)
1247
    {
1248
        $event = $this->dispatch($eventName);
1249
1250
        return !$event->isPropagationStopped();
1251
    }
1252
1253
    /////////////////////////////
1254
    // Validation
1255
    /////////////////////////////
1256
1257
    /**
1258
     * Gets the error stack for this model instance. Used to
1259
     * keep track of validation errors.
1260
     *
1261
     * @return \Infuse\ErrorStack
1262
     */
1263
    public function getErrors()
1264
    {
1265
        if (!$this->_errors) {
1266
            $this->_errors = new ErrorStack($this->app);
1267
        }
1268
1269
        return $this->_errors;
1270
    }
1271
1272
    /**
1273
     * Validates and marshals a value to storage.
1274
     *
1275
     * @param array  $property
1276
     * @param string $propertyName
1277
     * @param mixed  $value
1278
     *
1279
     * @return bool
1280
     */
1281
    private function filterAndValidate(array $property, $propertyName, &$value)
1282
    {
1283
        // assume empty string is a null value for properties
1284
        // that are marked as optionally-null
1285
        if ($property['null'] && empty($value)) {
1286
            $value = null;
1287
1288
            return true;
1289
        }
1290
1291
        // validate
1292
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1293
1294
        // unique?
1295
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1296
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1297
        }
1298
1299
        return $valid;
1300
    }
1301
1302
    /**
1303
     * Validates a value for a property.
1304
     *
1305
     * @param array  $property
1306
     * @param string $propertyName
1307
     * @param mixed  $value
1308
     *
1309
     * @return bool
1310
     */
1311
    private function validate(array $property, $propertyName, $value)
1312
    {
1313
        $valid = true;
1314
1315
        if (isset($property['validate']) && is_callable($property['validate'])) {
1316
            $valid = call_user_func_array($property['validate'], [$value]);
1317
        } elseif (isset($property['validate'])) {
1318
            $valid = Validate::is($value, $property['validate']);
1319
        }
1320
1321
        if (!$valid) {
1322
            $this->getErrors()->push([
1323
                'error' => self::ERROR_VALIDATION_FAILED,
1324
                'params' => [
1325
                    'field' => $propertyName,
1326
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1327
        }
1328
1329
        return [$valid, $value];
1330
    }
1331
1332
    /**
1333
     * Checks if a value is unique for a property.
1334
     *
1335
     * @param array  $property
1336
     * @param string $propertyName
1337
     * @param mixed  $value
1338
     *
1339
     * @return bool
1340
     */
1341
    private function checkUniqueness(array $property, $propertyName, $value)
1342
    {
1343
        if (static::totalRecords([$propertyName => $value]) > 0) {
1344
            $this->getErrors()->push([
1345
                'error' => self::ERROR_NOT_UNIQUE,
1346
                'params' => [
1347
                    'field' => $propertyName,
1348
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1349
1350
            return false;
1351
        }
1352
1353
        return true;
1354
    }
1355
1356
    /**
1357
     * Checks if an input has all of the required values. Adds
1358
     * messages for any missing values to the error stack.
1359
     *
1360
     * @param array $values
1361
     *
1362
     * @return bool
1363
     */
1364
    private function hasRequiredValues(array $values)
1365
    {
1366
        $hasRequired = true;
1367
        foreach (static::$properties as $name => $property) {
1368
            if ($property['required'] && !isset($values[$name])) {
1369
                $property = static::$properties[$name];
1370
                $this->getErrors()->push([
1371
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
1372
                    'params' => [
1373
                        'field' => $name,
1374
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
1375
1376
                $hasRequired = false;
1377
            }
1378
        }
1379
1380
        return $hasRequired;
1381
    }
1382
1383
    /**
1384
     * Gets the marshaled default value for a property (if set).
1385
     *
1386
     * @param string $property
1387
     *
1388
     * @return mixed
1389
     */
1390
    private function getPropertyDefault(array $property)
1391
    {
1392
        return array_value($property, 'default');
1393
    }
1394
}
1395