Completed
Push — master ( e0f9e9...43f62d )
by Jared
175:39 queued 119:08
created

Model::get()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
804
        }
805
806
        return $result;
807
    }
808
809
    /**
810
     * Converts the model to an array.
811
     *
812
     * @param array $exclude properties to exclude
813
     * @param array $include properties to include
814
     * @param array $expand  properties to expand
815
     *
816
     * @return array properties
817
     */
818
    public function toArrayDeprecated(array $exclude = [], array $include = [], array $expand = [])
819
    {
820
        // start with the base array representation of this object
821
        $result = $this->toArray();
822
823
        // apply namespacing to $exclude
824
        $namedExc = [];
825
        foreach ($exclude as $e) {
826
            array_set($namedExc, $e, true);
827
        }
828
829
        // apply namespacing to $include
830
        $namedInc = [];
831
        foreach ($include as $e) {
832
            array_set($namedInc, $e, true);
833
        }
834
835
        // apply namespacing to $expand
836
        $namedExp = [];
837
        foreach ($expand as $e) {
838
            array_set($namedExp, $e, true);
839
        }
840
841
        // remove excluded properties
842
        foreach (array_keys($result) as $k) {
843
            if (isset($namedExc[$k]) && !is_array($namedExc[$k])) {
844
                unset($result[$k]);
845
            }
846
        }
847
848
        // add included properties
849
        foreach (array_keys($namedInc) as $k) {
850
            if (!isset($result[$k]) && isset($namedInc[$k]) && self::hasProperty($k)) {
851
                $result[$k] = $this->$k;
852
            }
853
        }
854
855
        // expand any relational model properties
856
        $result = $this->toArrayExpand($result, $namedExc, $namedInc, $namedExp);
857
858
        // apply hooks, if available
859
        if (method_exists($this, 'toArrayHook')) {
860
            $this->toArrayHook($result, $namedExc, $namedInc, $namedExp);
0 ignored issues
show
Bug introduced by
The method toArrayHook() does not exist on Pulsar\Model. Did you maybe mean toArray()?

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

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

Loading history...
861
        }
862
863
        return $result;
864
    }
865
866
    /**
867
     * Expands any relational properties within a result.
868
     *
869
     * @param array $result
870
     * @param array $namedExc
871
     * @param array $namedInc
872
     * @param array $namedExp
873
     *
874
     * @return array
875
     */
876
    private function toArrayExpand(array $result, array $namedExc, array $namedInc, array $namedExp)
877
    {
878
        foreach ($namedExp as $k => $subExp) {
879
            // if not a property, or the value is null is null, excluded, or not included
880
            // then we are not going to expand it
881
            if (!static::hasProperty($k) || !isset($result[$k]) || !$result[$k]) {
882
                continue;
883
            }
884
885
            $subExc = array_value($namedExc, $k);
886
            $subInc = array_value($namedInc, $k);
887
888
            // convert exclude, include, and expand into dot notation
889
            // then take the keys for a flattened dot notation
890
            $flatExc = is_array($subExc) ? array_keys(array_dot($subExc)) : [];
891
            $flatInc = is_array($subInc) ? array_keys(array_dot($subInc)) : [];
892
            $flatExp = is_array($subExp) ? array_keys(array_dot($subExp)) : [];
893
894
            $relation = $this->relation($k);
895
            $result[$k] = $relation->toArrayDeprecated($flatExc, $flatInc, $flatExp);
896
        }
897
898
        return $result;
899
    }
900
901
    /**
902
     * Updates the model.
903
     *
904
     * @param array $data optional key-value properties to set
905
     *
906
     * @return bool
907
     *
908
     * @throws BadMethodCallException when not called on an existing model
909
     */
910
    public function set(array $data = [])
911
    {
912
        if ($this->_id === false) {
913
            throw new BadMethodCallException('Can only call set() on an existing model');
914
        }
915
916
        // not updating anything?
917
        if (count($data) == 0) {
918
            return true;
919
        }
920
921
        // apply mutators
922
        foreach ($data as $k => $value) {
923
            if ($mutator = self::getMutator($k)) {
924
                $data[$k] = $this->$mutator($value);
925
            }
926
        }
927
928
        // dispatch the model.updating event
929
        if (!$this->beforeUpdate($data)) {
930
            return false;
931
        }
932
933
        // validate the values being saved
934
        $validated = true;
935
        $updateArray = [];
936
        foreach ($data as $name => $value) {
937
            // exclude if value does not map to a property
938
            if (!isset(static::$properties[$name])) {
939
                continue;
940
            }
941
942
            $property = static::$properties[$name];
943
944
            // can only modify mutable properties
945
            if ($property['mutable'] != self::MUTABLE) {
946
                continue;
947
            }
948
949
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
950
            $updateArray[$name] = $value;
951
        }
952
953
        if (!$validated) {
954
            return false;
955
        }
956
957
        $updated = self::$driver->updateModel($this, $updateArray);
958
959
        if ($updated) {
960
            // NOTE clear the local cache before the model.updated
961
            // event so that fetching values forces a reload
962
            // from the storage layer
963
            $this->clearCache();
964
965
            // dispatch the model.updated event
966
            if (!$this->afterUpdate()) {
967
                return false;
968
            }
969
        }
970
971
        return $updated;
972
    }
973
974
    /**
975
     * Delete the model.
976
     *
977
     * @return bool success
978
     */
979
    public function delete()
980
    {
981
        if ($this->_id === false) {
982
            throw new BadMethodCallException('Can only call delete() on an existing model');
983
        }
984
985
        // dispatch the model.deleting event
986
        if (!$this->beforeDelete()) {
987
            return false;
988
        }
989
990
        $deleted = self::$driver->deleteModel($this);
991
992
        if ($deleted) {
993
            // dispatch the model.deleted event
994
            if (!$this->afterDelete()) {
995
                return false;
996
            }
997
998
            // NOTE clear the local cache before the model.deleted
999
            // event so that fetching values forces a reload
1000
            // from the storage layer
1001
            $this->clearCache();
1002
        }
1003
1004
        return $deleted;
1005
    }
1006
1007
    /////////////////////////////
1008
    // Queries
1009
    /////////////////////////////
1010
1011
    /**
1012
     * Generates a new query instance.
1013
     *
1014
     * @return Model\Query
1015
     */
1016
    public static function query()
1017
    {
1018
        // Create a new model instance for the query to ensure
1019
        // that the model's initialize() method gets called.
1020
        // Otherwise, the property definitions will be incomplete.
1021
        $model = new static();
1022
1023
        return new Query($model);
1024
    }
1025
1026
    /**
1027
     * Gets the toal number of records matching an optional criteria.
1028
     *
1029
     * @param array $where criteria
1030
     *
1031
     * @return int total
1032
     */
1033
    public static function totalRecords(array $where = [])
1034
    {
1035
        $query = static::query();
1036
        $query->where($where);
1037
1038
        return self::getDriver()->totalRecords($query);
1039
    }
1040
1041
    /**
1042
     * Checks if the model exists in the database.
1043
     *
1044
     * @return bool
1045
     */
1046
    public function exists()
1047
    {
1048
        return static::totalRecords($this->ids()) == 1;
1049
    }
1050
1051
    /**
1052
     * @deprecated alias for refresh()
1053
     */
1054
    public function load()
1055
    {
1056
        return $this->refresh();
1057
    }
1058
1059
    /**
1060
     * Loads the model from the storage layer.
1061
     *
1062
     * @return self
1063
     */
1064
    public function refresh()
1065
    {
1066
        if ($this->_id === false) {
1067
            return $this;
1068
        }
1069
1070
        $values = self::$driver->loadModel($this);
1071
1072
        if (!is_array($values)) {
1073
            return $this;
1074
        }
1075
1076
        // clear any relations
1077
        $this->_relationships = [];
1078
1079
        return $this->refreshWith($values);
1080
    }
1081
1082
    /**
1083
     * Loads values into the model.
1084
     *
1085
     * @param array $values values
1086
     *
1087
     * @return self
1088
     */
1089
    public function refreshWith(array $values)
1090
    {
1091
        $this->_values = $values;
1092
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * Clears the cache for this model.
1098
     *
1099
     * @return self
1100
     */
1101
    public function clearCache()
1102
    {
1103
        $this->_unsaved = [];
1104
        $this->_values = [];
1105
        $this->_relationships = [];
1106
1107
        return $this;
1108
    }
1109
1110
    /////////////////////////////
1111
    // Relationships
1112
    /////////////////////////////
1113
1114
    /**
1115
     * Gets the model object corresponding to a relation
1116
     * WARNING no check is used to see if the model returned actually exists.
1117
     *
1118
     * @param string $propertyName property
1119
     *
1120
     * @return \Pulsar\Model model
1121
     */
1122
    public function relation($propertyName)
1123
    {
1124
        // TODO deprecated
1125
        $property = static::getProperty($propertyName);
1126
1127
        if (!isset($this->_relationships[$propertyName])) {
1128
            $relationModelName = $property['relation'];
1129
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1130
        }
1131
1132
        return $this->_relationships[$propertyName];
1133
    }
1134
1135
    /**
1136
     * Creates the parent side of a One-To-One relationship.
1137
     *
1138
     * @param string $model      foreign model class
1139
     * @param string $foreignKey identifying key on foreign model
1140
     * @param string $localKey   identifying key on local model
1141
     *
1142
     * @return Relation
1143
     */
1144 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...
1145
    {
1146
        // the default local key would look like `user_id`
1147
        // for a model named User
1148
        if (!$foreignKey) {
1149
            $inflector = Inflector::get();
1150
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1151
        }
1152
1153
        if (!$localKey) {
1154
            $localKey = self::DEFAULT_ID_PROPERTY;
1155
        }
1156
1157
        return new HasOne($model, $foreignKey, $localKey, $this);
1158
    }
1159
1160
    /**
1161
     * Creates the child side of a One-To-One or One-To-Many relationship.
1162
     *
1163
     * @param string $model      foreign model class
1164
     * @param string $foreignKey identifying key on foreign model
1165
     * @param string $localKey   identifying key on local model
1166
     *
1167
     * @return Relation
1168
     */
1169 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...
1170
    {
1171
        if (!$foreignKey) {
1172
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1173
        }
1174
1175
        // the default local key would look like `user_id`
1176
        // for a model named User
1177
        if (!$localKey) {
1178
            $inflector = Inflector::get();
1179
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1180
        }
1181
1182
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1183
    }
1184
1185
    /**
1186
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1187
     *
1188
     * @param string $model      foreign model class
1189
     * @param string $foreignKey identifying key on foreign model
1190
     * @param string $localKey   identifying key on local model
1191
     *
1192
     * @return Relation
1193
     */
1194 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...
1195
    {
1196
        // the default local key would look like `user_id`
1197
        // for a model named User
1198
        if (!$foreignKey) {
1199
            $inflector = Inflector::get();
1200
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1201
        }
1202
1203
        if (!$localKey) {
1204
            $localKey = self::DEFAULT_ID_PROPERTY;
1205
        }
1206
1207
        return new HasMany($model, $foreignKey, $localKey, $this);
1208
    }
1209
1210
    /**
1211
     * Creates the child side of a Many-To-Many relationship.
1212
     *
1213
     * @param string $model      foreign model class
1214
     * @param string $foreignKey identifying key on foreign model
1215
     * @param string $localKey   identifying key on local model
1216
     *
1217
     * @return Relation
1218
     */
1219 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...
1220
    {
1221
        if (!$foreignKey) {
1222
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1223
        }
1224
1225
        // the default local key would look like `user_id`
1226
        // for a model named User
1227
        if (!$localKey) {
1228
            $inflector = Inflector::get();
1229
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1230
        }
1231
1232
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1233
    }
1234
1235
    /////////////////////////////
1236
    // Events
1237
    /////////////////////////////
1238
1239
    /**
1240
     * Gets the event dispatcher.
1241
     *
1242
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1243
     */
1244
    public static function getDispatcher($ignoreCache = false)
1245
    {
1246
        $class = get_called_class();
1247
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1248
            self::$dispatchers[$class] = new EventDispatcher();
1249
        }
1250
1251
        return self::$dispatchers[$class];
1252
    }
1253
1254
    /**
1255
     * Subscribes to a listener to an event.
1256
     *
1257
     * @param string   $event    event name
1258
     * @param callable $listener
1259
     * @param int      $priority optional priority, higher #s get called first
1260
     */
1261
    public static function listen($event, callable $listener, $priority = 0)
1262
    {
1263
        static::getDispatcher()->addListener($event, $listener, $priority);
1264
    }
1265
1266
    /**
1267
     * Adds a listener to the model.creating event.
1268
     *
1269
     * @param callable $listener
1270
     * @param int      $priority
1271
     */
1272
    public static function creating(callable $listener, $priority = 0)
1273
    {
1274
        static::listen(ModelEvent::CREATING, $listener, $priority);
1275
    }
1276
1277
    /**
1278
     * Adds a listener to the model.created event.
1279
     *
1280
     * @param callable $listener
1281
     * @param int      $priority
1282
     */
1283
    public static function created(callable $listener, $priority = 0)
1284
    {
1285
        static::listen(ModelEvent::CREATED, $listener, $priority);
1286
    }
1287
1288
    /**
1289
     * Adds a listener to the model.updating event.
1290
     *
1291
     * @param callable $listener
1292
     * @param int      $priority
1293
     */
1294
    public static function updating(callable $listener, $priority = 0)
1295
    {
1296
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1297
    }
1298
1299
    /**
1300
     * Adds a listener to the model.updated event.
1301
     *
1302
     * @param callable $listener
1303
     * @param int      $priority
1304
     */
1305
    public static function updated(callable $listener, $priority = 0)
1306
    {
1307
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1308
    }
1309
1310
    /**
1311
     * Adds a listener to the model.deleting event.
1312
     *
1313
     * @param callable $listener
1314
     * @param int      $priority
1315
     */
1316
    public static function deleting(callable $listener, $priority = 0)
1317
    {
1318
        static::listen(ModelEvent::DELETING, $listener, $priority);
1319
    }
1320
1321
    /**
1322
     * Adds a listener to the model.deleted event.
1323
     *
1324
     * @param callable $listener
1325
     * @param int      $priority
1326
     */
1327
    public static function deleted(callable $listener, $priority = 0)
1328
    {
1329
        static::listen(ModelEvent::DELETED, $listener, $priority);
1330
    }
1331
1332
    /**
1333
     * Dispatches an event.
1334
     *
1335
     * @param string $eventName
1336
     *
1337
     * @return Model\ModelEvent
1338
     */
1339
    protected function dispatch($eventName)
1340
    {
1341
        $event = new ModelEvent($this);
1342
1343
        return static::getDispatcher()->dispatch($eventName, $event);
1344
    }
1345
1346
    /**
1347
     * Dispatches the model.creating event.
1348
     *
1349
     * @return bool
1350
     */
1351 View Code Duplication
    private function beforeCreate()
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...
1352
    {
1353
        $event = $this->dispatch(ModelEvent::CREATING);
1354
        if ($event->isPropagationStopped()) {
1355
            return false;
1356
        }
1357
1358
        // TODO deprecated
1359
        if (method_exists($this, 'preCreateHook') && !$this->preCreateHook($this->_unsaved)) {
0 ignored issues
show
Bug introduced by
The method preCreateHook() does not exist on Pulsar\Model. Did you maybe mean create()?

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

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

Loading history...
1360
            return false;
1361
        }
1362
1363
        return true;
1364
    }
1365
1366
    /**
1367
     * Dispatches the model.created event.
1368
     *
1369
     * @return bool
1370
     */
1371 View Code Duplication
    private function afterCreate()
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...
1372
    {
1373
        $event = $this->dispatch(ModelEvent::CREATED);
1374
        if ($event->isPropagationStopped()) {
1375
            return false;
1376
        }
1377
1378
        // TODO deprecated
1379
        if (method_exists($this, 'postCreateHook') && $this->postCreateHook() === false) {
0 ignored issues
show
Bug introduced by
The method postCreateHook() does not exist on Pulsar\Model. Did you maybe mean create()?

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

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

Loading history...
1380
            return false;
1381
        }
1382
1383
        return true;
1384
    }
1385
1386
    /**
1387
     * Dispatches the model.updating event.
1388
     *
1389
     * @param array $data
1390
     *
1391
     * @return bool
1392
     */
1393 View Code Duplication
    private function beforeUpdate(array &$data)
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...
1394
    {
1395
        $event = $this->dispatch(ModelEvent::UPDATING);
1396
        if ($event->isPropagationStopped()) {
1397
            return false;
1398
        }
1399
1400
        // TODO deprecated
1401
        if (method_exists($this, 'preSetHook') && !$this->preSetHook($data)) {
0 ignored issues
show
Bug introduced by
The method preSetHook() does not seem to exist on object<Pulsar\Model>.

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

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

Loading history...
1402
            return false;
1403
        }
1404
1405
        return true;
1406
    }
1407
1408
    /**
1409
     * Dispatches the model.updated event.
1410
     *
1411
     * @return bool
1412
     */
1413 View Code Duplication
    private function afterUpdate()
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...
1414
    {
1415
        $event = $this->dispatch(ModelEvent::UPDATED);
1416
        if ($event->isPropagationStopped()) {
1417
            return false;
1418
        }
1419
1420
        // TODO deprecated
1421
        if (method_exists($this, 'postSetHook') && $this->postSetHook() === false) {
0 ignored issues
show
Bug introduced by
The method postSetHook() does not seem to exist on object<Pulsar\Model>.

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

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

Loading history...
1422
            return false;
1423
        }
1424
1425
        return true;
1426
    }
1427
1428
    /**
1429
     * Dispatches the model.deleting event.
1430
     *
1431
     * @return bool
1432
     */
1433 View Code Duplication
    private function beforeDelete()
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...
1434
    {
1435
        $event = $this->dispatch(ModelEvent::DELETING);
1436
        if ($event->isPropagationStopped()) {
1437
            return false;
1438
        }
1439
1440
        // TODO deprecated
1441
        if (method_exists($this, 'preDeleteHook') && !$this->preDeleteHook()) {
0 ignored issues
show
Bug introduced by
The method preDeleteHook() does not exist on Pulsar\Model. Did you maybe mean delete()?

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

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

Loading history...
1442
            return false;
1443
        }
1444
1445
        return true;
1446
    }
1447
1448
    /**
1449
     * Dispatches the model.created event.
1450
     *
1451
     * @return bool
1452
     */
1453 View Code Duplication
    private function afterDelete()
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...
1454
    {
1455
        $event = $this->dispatch(ModelEvent::DELETED);
1456
        if ($event->isPropagationStopped()) {
1457
            return false;
1458
        }
1459
1460
        // TODO deprecated
1461
        if (method_exists($this, 'postDeleteHook') && $this->postDeleteHook() === false) {
0 ignored issues
show
Bug introduced by
The method postDeleteHook() does not exist on Pulsar\Model. Did you maybe mean delete()?

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

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

Loading history...
1462
            return false;
1463
        }
1464
1465
        return true;
1466
    }
1467
1468
    /////////////////////////////
1469
    // Validation
1470
    /////////////////////////////
1471
1472
    /**
1473
     * Validates and marshals a value to storage.
1474
     *
1475
     * @param array  $property
1476
     * @param string $propertyName
1477
     * @param mixed  $value
1478
     *
1479
     * @return bool
1480
     */
1481
    private function filterAndValidate(array $property, $propertyName, &$value)
1482
    {
1483
        // assume empty string is a null value for properties
1484
        // that are marked as optionally-null
1485
        if ($property['null'] && empty($value)) {
1486
            $value = null;
1487
1488
            return true;
1489
        }
1490
1491
        // validate
1492
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1493
1494
        // unique?
1495
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1496
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1497
        }
1498
1499
        return $valid;
1500
    }
1501
1502
    /**
1503
     * Validates a value for a property.
1504
     *
1505
     * @param array  $property
1506
     * @param string $propertyName
1507
     * @param mixed  $value
1508
     *
1509
     * @return bool
1510
     */
1511
    private function validate(array $property, $propertyName, $value)
1512
    {
1513
        $valid = true;
1514
1515
        if (isset($property['validate']) && is_callable($property['validate'])) {
1516
            $valid = call_user_func_array($property['validate'], [$value]);
1517
        } elseif (isset($property['validate'])) {
1518
            $valid = Validate::is($value, $property['validate']);
1519
        }
1520
1521
        if (!$valid) {
1522
            $this->app['errors']->push([
1523
                'error' => self::ERROR_VALIDATION_FAILED,
1524
                'params' => [
1525
                    'field' => $propertyName,
1526
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1527
        }
1528
1529
        return [$valid, $value];
1530
    }
1531
1532
    /**
1533
     * Checks if a value is unique for a property.
1534
     *
1535
     * @param array  $property
1536
     * @param string $propertyName
1537
     * @param mixed  $value
1538
     *
1539
     * @return bool
1540
     */
1541
    private function checkUniqueness(array $property, $propertyName, $value)
1542
    {
1543
        if (static::totalRecords([$propertyName => $value]) > 0) {
1544
            $this->app['errors']->push([
1545
                'error' => self::ERROR_NOT_UNIQUE,
1546
                'params' => [
1547
                    'field' => $propertyName,
1548
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1549
1550
            return false;
1551
        }
1552
1553
        return true;
1554
    }
1555
1556
    /**
1557
     * Gets the marshaled default value for a property (if set).
1558
     *
1559
     * @param string $property
1560
     *
1561
     * @return mixed
1562
     */
1563
    private function getPropertyDefault(array $property)
1564
    {
1565
        return array_value($property, 'default');
1566
    }
1567
}
1568