Completed
Push — master ( 69b851...37ffb9 )
by Jared
01:36
created

Model::initialize()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 30
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.439
c 0
b 0
f 0
cc 6
eloc 11
nc 16
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use BadMethodCallException;
15
use ICanBoogie\Inflector;
16
use Pimple\Container;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Exception\DriverMissingException;
19
use Pulsar\Exception\MassAssignmentException;
20
use Pulsar\Exception\ModelException;
21
use Pulsar\Exception\ModelNotFoundException;
22
use Pulsar\Relation\BelongsTo;
23
use Pulsar\Relation\BelongsToMany;
24
use Pulsar\Relation\HasMany;
25
use Pulsar\Relation\HasOne;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
28
/**
29
 * Class Model.
30
 */
31
abstract class Model implements \ArrayAccess
32
{
33
    const IMMUTABLE = 0;
34
    const MUTABLE_CREATE_ONLY = 1;
35
    const MUTABLE = 2;
36
37
    const TYPE_STRING = 'string';
38
    const TYPE_NUMBER = 'number'; // DEPRECATED
39
    const TYPE_INTEGER = 'integer';
40
    const TYPE_FLOAT = 'float';
41
    const TYPE_BOOLEAN = 'boolean';
42
    const TYPE_DATE = 'date';
43
    const TYPE_OBJECT = 'object';
44
    const TYPE_ARRAY = 'array';
45
46
    const ERROR_REQUIRED_FIELD_MISSING = 'required_field_missing';
47
    const ERROR_VALIDATION_FAILED = 'validation_failed';
48
    const ERROR_NOT_UNIQUE = 'not_unique';
49
50
    const DEFAULT_ID_PROPERTY = 'id';
51
52
    /////////////////////////////
53
    // Model visible variables
54
    /////////////////////////////
55
56
    /**
57
     * List of model ID property names.
58
     *
59
     * @var array
60
     */
61
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
62
63
    /**
64
     * Property definitions expressed as a key-value map with
65
     * property names as the keys.
66
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
67
     *
68
     * @var array
69
     */
70
    protected static $properties = [];
71
72
    /**
73
     * @var array
74
     */
75
    protected static $dispatchers;
76
77
    /**
78
     * @var Container
79
     */
80
    protected static $globalContainer;
81
82
    /**
83
     * @var ErrorStack
84
     */
85
    protected static $globalErrorStack;
86
87
    /**
88
     * @var number|string|false
89
     */
90
    protected $_id;
91
92
    /**
93
     * @var array
94
     */
95
    protected $_ids;
96
97
    /**
98
     * @var array
99
     */
100
    protected $_values = [];
101
102
    /**
103
     * @var array
104
     */
105
    protected $_unsaved = [];
106
107
    /**
108
     * @var bool
109
     */
110
    protected $_persisted = false;
111
112
    /**
113
     * @var array
114
     */
115
    protected $_relationships = [];
116
117
    /**
118
     * @var ErrorStack
119
     */
120
    protected $_errors;
121
122
    /////////////////////////////
123
    // Base model variables
124
    /////////////////////////////
125
126
    /**
127
     * @var array
128
     */
129
    private static $propertyDefinitionBase = [
130
        'type' => self::TYPE_STRING,
131
        'mutable' => self::MUTABLE,
132
        'null' => false,
133
        'unique' => false,
134
        'required' => false,
135
    ];
136
137
    /**
138
     * @var array
139
     */
140
    private static $defaultIDProperty = [
141
        'type' => self::TYPE_INTEGER,
142
        'mutable' => self::IMMUTABLE,
143
    ];
144
145
    /**
146
     * @var array
147
     */
148
    private static $timestampProperties = [
149
        'created_at' => [
150
            'type' => self::TYPE_DATE,
151
            'validate' => 'timestamp|db_timestamp',
152
        ],
153
        'updated_at' => [
154
            'type' => self::TYPE_DATE,
155
            'validate' => 'timestamp|db_timestamp',
156
        ],
157
    ];
158
159
    /**
160
     * @var array
161
     */
162
    private static $softDeleteProperties = [
163
        'deleted_at' => [
164
            'type' => self::TYPE_DATE,
165
            'validate' => 'timestamp|db_timestamp',
166
            'null' => true,
167
        ],
168
    ];
169
170
    /**
171
     * @var array
172
     */
173
    private static $initialized = [];
174
175
    /**
176
     * @var DriverInterface
177
     */
178
    private static $driver;
179
180
    /**
181
     * @var array
182
     */
183
    private static $accessors = [];
184
185
    /**
186
     * @var array
187
     */
188
    private static $mutators = [];
189
190
    /**
191
     * @var bool
192
     */
193
    private $_ignoreUnsaved;
194
195
    /**
196
     * Creates a new model object.
197
     *
198
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
199
     * @param array                    $values optional key-value map to pre-seed model
200
     */
201
    public function __construct($id = false, array $values = [])
202
    {
203
        // initialize the model
204
        $this->init();
205
206
        // parse the supplied model ID
207
        if (is_array($id)) {
208
            // A model can be supplied as a primary key
209
            foreach ($id as &$el) {
210
                if ($el instanceof self) {
211
                    $el = $el->id();
212
                }
213
            }
214
215
            // The IDs come in as the same order as ::$ids.
216
            // We need to match up the elements on that
217
            // input into a key-value map for each ID property.
218
            $ids = [];
219
            $idQueue = array_reverse($id);
220
            foreach (static::$ids as $k => $f) {
221
                $ids[$f] = (count($idQueue) > 0) ? array_pop($idQueue) : false;
222
            }
223
224
            $this->_id = implode(',', $id);
225
            $this->_ids = $ids;
226
        } elseif ($id instanceof self) {
227
            // A model can be supplied as a primary key
228
            $this->_id = $id->id();
229
            $this->_ids = $id->ids();
230
        } else {
231
            $this->_id = $id;
232
            $idProperty = static::$ids[0];
233
            $this->_ids = [$idProperty => $id];
234
        }
235
236
        // load any given values
237
        if (count($values) > 0) {
238
            $this->refreshWith($values);
239
        }
240
    }
241
242
    /**
243
     * Performs initialization on this model.
244
     */
245
    private function init()
246
    {
247
        // ensure the initialize function is called only once
248
        $k = get_called_class();
249
        if (!isset(self::$initialized[$k])) {
250
            $this->initialize();
251
            self::$initialized[$k] = true;
252
        }
253
    }
254
255
    /**
256
     * The initialize() method is called once per model. It's used
257
     * to perform any one-off tasks before the model gets
258
     * constructed. This is a great place to add any model
259
     * properties. When extending this method be sure to call
260
     * parent::initialize() as some important stuff happens here.
261
     * If extending this method to add properties then you should
262
     * call parent::initialize() after adding any properties.
263
     */
264
    protected function initialize()
265
    {
266
        // load the driver
267
        static::getDriver();
268
269
        // add in the default ID property
270
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
271
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
272
        }
273
274
        // generates created_at and updated_at timestamps
275
        if (property_exists($this, 'autoTimestamps')) {
276
            $this->installAutoTimestamps();
277
        }
278
279
        // generates deleted_at timestamps
280
        if (property_exists($this, 'softDelete')) {
281
            $this->installSoftDelete();
282
        }
283
284
        // fill in each property by extending the property
285
        // definition base
286
        foreach (static::$properties as &$property) {
287
            $property = array_replace(self::$propertyDefinitionBase, $property);
288
        }
289
290
        // order the properties array by name for consistency
291
        // since it is constructed in a random order
292
        ksort(static::$properties);
293
    }
294
295
    /**
296
     * Installs the `created_at` and `updated_at` properties.
297
     */
298
    private function installAutoTimestamps()
299
    {
300
        static::$properties = array_replace(self::$timestampProperties, static::$properties);
301
302
        self::creating(function (ModelEvent $event) {
303
            $model = $event->getModel();
304
            $model->created_at = time();
0 ignored issues
show
Documentation introduced by
The property created_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
305
            $model->updated_at = time();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
306
        });
307
308
        self::updating(function (ModelEvent $event) {
309
            $event->getModel()->updated_at = time();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
310
        });
311
    }
312
313
    /**
314
     * Installs the `deleted_at` properties.
315
     */
316
    private function installSoftDelete()
317
    {
318
        static::$properties = array_replace(self::$softDeleteProperties, static::$properties);
319
    }
320
321
    /**
322
     * @deprecated
323
     *
324
     * Injects a DI container
325
     *
326
     * @param Container $container
327
     */
328
    public static function inject(Container $container)
329
    {
330
        self::$globalContainer = $container;
331
    }
332
333
    /**
334
     * @deprecated
335
     *
336
     * Gets the DI container used for this model
337
     *
338
     * @return Container|null
339
     */
340
    public function getApp()
341
    {
342
        return self::$globalContainer;
343
    }
344
345
    /**
346
     * Sets the driver for all models.
347
     *
348
     * @param DriverInterface $driver
349
     */
350
    public static function setDriver(DriverInterface $driver)
351
    {
352
        self::$driver = $driver;
353
    }
354
355
    /**
356
     * Gets the driver for all models.
357
     *
358
     * @return DriverInterface
359
     *
360
     * @throws DriverMissingException when a driver has not been set yet
361
     */
362
    public static function getDriver()
363
    {
364
        if (!self::$driver) {
365
            throw new DriverMissingException('A model driver has not been set yet.');
366
        }
367
368
        return self::$driver;
369
    }
370
371
    /**
372
     * Clears the driver for all models.
373
     */
374
    public static function clearDriver()
375
    {
376
        self::$driver = null;
377
    }
378
379
    /**
380
     * Gets the name of the model, i.e. User.
381
     *
382
     * @return string
383
     */
384
    public static function modelName()
385
    {
386
        // strip namespacing
387
        $paths = explode('\\', get_called_class());
388
389
        return end($paths);
390
    }
391
392
    /**
393
     * Gets the model ID.
394
     *
395
     * @return string|number|false ID
396
     */
397
    public function id()
398
    {
399
        return $this->_id;
400
    }
401
402
    /**
403
     * Gets a key-value map of the model ID.
404
     *
405
     * @return array ID map
406
     */
407
    public function ids()
408
    {
409
        return $this->_ids;
410
    }
411
412
    /**
413
     * Sets the global error stack instance.
414
     *
415
     * @param ErrorStack $stack
416
     */
417
    public static function setErrorStack(ErrorStack $stack)
418
    {
419
        self::$globalErrorStack = $stack;
420
    }
421
422
    /**
423
     * Clears the global error stack instance.
424
     */
425
    public static function clearErrorStack()
426
    {
427
        self::$globalErrorStack = null;
428
    }
429
430
    /////////////////////////////
431
    // Magic Methods
432
    /////////////////////////////
433
434
    /**
435
     * Converts the model into a string.
436
     *
437
     * @return string
438
     */
439
    public function __toString()
440
    {
441
        return get_called_class().'('.$this->_id.')';
442
    }
443
444
    /**
445
     * Shortcut to a get() call for a given property.
446
     *
447
     * @param string $name
448
     *
449
     * @return mixed
450
     */
451
    public function __get($name)
452
    {
453
        $result = $this->get([$name]);
454
455
        return reset($result);
456
    }
457
458
    /**
459
     * Sets an unsaved value.
460
     *
461
     * @param string $name
462
     * @param mixed  $value
463
     */
464
    public function __set($name, $value)
465
    {
466
        // if changing property, remove relation model
467
        if (isset($this->_relationships[$name])) {
468
            unset($this->_relationships[$name]);
469
        }
470
471
        // call any mutators
472
        $mutator = self::getMutator($name);
473
        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...
474
            $this->_unsaved[$name] = $this->$mutator($value);
475
        } else {
476
            $this->_unsaved[$name] = $value;
477
        }
478
    }
479
480
    /**
481
     * Checks if an unsaved value or property exists by this name.
482
     *
483
     * @param string $name
484
     *
485
     * @return bool
486
     */
487
    public function __isset($name)
488
    {
489
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
490
    }
491
492
    /**
493
     * Unsets an unsaved value.
494
     *
495
     * @param string $name
496
     */
497
    public function __unset($name)
498
    {
499
        if (array_key_exists($name, $this->_unsaved)) {
500
            // if changing property, remove relation model
501
            if (isset($this->_relationships[$name])) {
502
                unset($this->_relationships[$name]);
503
            }
504
505
            unset($this->_unsaved[$name]);
506
        }
507
    }
508
509
    /////////////////////////////
510
    // ArrayAccess Interface
511
    /////////////////////////////
512
513
    public function offsetExists($offset)
514
    {
515
        return isset($this->$offset);
516
    }
517
518
    public function offsetGet($offset)
519
    {
520
        return $this->$offset;
521
    }
522
523
    public function offsetSet($offset, $value)
524
    {
525
        $this->$offset = $value;
526
    }
527
528
    public function offsetUnset($offset)
529
    {
530
        unset($this->$offset);
531
    }
532
533
    public static function __callStatic($name, $parameters)
534
    {
535
        // Any calls to unkown static methods should be deferred to
536
        // the query. This allows calls like User::where()
537
        // 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...
538
        return call_user_func_array([static::query(), $name], $parameters);
539
    }
540
541
    /////////////////////////////
542
    // Property Definitions
543
    /////////////////////////////
544
545
    /**
546
     * Gets all the property definitions for the model.
547
     *
548
     * @return array key-value map of properties
549
     */
550
    public static function getProperties()
551
    {
552
        return static::$properties;
553
    }
554
555
    /**
556
     * Gets a property defition for the model.
557
     *
558
     * @param string $property property to lookup
559
     *
560
     * @return array|null property
561
     */
562
    public static function getProperty($property)
563
    {
564
        return array_value(static::$properties, $property);
565
    }
566
567
    /**
568
     * Gets the names of the model ID properties.
569
     *
570
     * @return array
571
     */
572
    public static function getIDProperties()
573
    {
574
        return static::$ids;
575
    }
576
577
    /**
578
     * Checks if the model has a property.
579
     *
580
     * @param string $property property
581
     *
582
     * @return bool has property
583
     */
584
    public static function hasProperty($property)
585
    {
586
        return isset(static::$properties[$property]);
587
    }
588
589
    /**
590
     * Gets the mutator method name for a given proeprty name.
591
     * Looks for methods in the form of `setPropertyValue`.
592
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
593
     *
594
     * @param string $property property
595
     *
596
     * @return string|false method name if it exists
597
     */
598 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...
599
    {
600
        $class = get_called_class();
601
602
        $k = $class.':'.$property;
603
        if (!array_key_exists($k, self::$mutators)) {
604
            $inflector = Inflector::get();
605
            $method = 'set'.$inflector->camelize($property).'Value';
606
607
            if (!method_exists($class, $method)) {
608
                $method = false;
609
            }
610
611
            self::$mutators[$k] = $method;
612
        }
613
614
        return self::$mutators[$k];
615
    }
616
617
    /**
618
     * Gets the accessor method name for a given proeprty name.
619
     * Looks for methods in the form of `getPropertyValue`.
620
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
621
     *
622
     * @param string $property property
623
     *
624
     * @return string|false method name if it exists
625
     */
626 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...
627
    {
628
        $class = get_called_class();
629
630
        $k = $class.':'.$property;
631
        if (!array_key_exists($k, self::$accessors)) {
632
            $inflector = Inflector::get();
633
            $method = 'get'.$inflector->camelize($property).'Value';
634
635
            if (!method_exists($class, $method)) {
636
                $method = false;
637
            }
638
639
            self::$accessors[$k] = $method;
640
        }
641
642
        return self::$accessors[$k];
643
    }
644
645
    /**
646
     * Marshals a value for a given property from storage.
647
     *
648
     * @param array $property
649
     * @param mixed $value
650
     *
651
     * @return mixed type-casted value
652
     */
653
    public static function cast(array $property, $value)
654
    {
655
        if ($value === null) {
656
            return;
657
        }
658
659
        // handle empty strings as null
660
        if ($property['null'] && $value == '') {
661
            return;
662
        }
663
664
        $type = array_value($property, 'type');
665
        $m = 'to_'.$type;
666
667
        if (!method_exists(Property::class, $m)) {
668
            return $value;
669
        }
670
671
        return Property::$m($value);
672
    }
673
674
    /////////////////////////////
675
    // CRUD Operations
676
    /////////////////////////////
677
678
    /**
679
     * Gets the tablename for storing this model.
680
     *
681
     * @return string
682
     */
683
    public function getTablename()
684
    {
685
        $inflector = Inflector::get();
686
687
        return $inflector->camelize($inflector->pluralize(static::modelName()));
688
    }
689
690
    /**
691
     * Gets the ID of the connection in the connection manager
692
     * that stores this model.
693
     *
694
     * @return string|false
695
     */
696
    public function getConnection()
697
    {
698
        return false;
699
    }
700
701
    /**
702
     * Saves the model.
703
     *
704
     * @return bool true when the operation was successful
705
     */
706
    public function save()
707
    {
708
        if ($this->_id === false) {
709
            return $this->create();
710
        }
711
712
        return $this->set();
713
    }
714
715
    /**
716
     * Saves the model. Throws an exception when the operation fails.
717
     *
718
     * @throws ModelException when the model cannot be saved
719
     */
720
    public function saveOrFail()
721
    {
722
        if (!$this->save()) {
723
            throw new ModelException('Failed to save '.static::modelName());
724
        }
725
    }
726
727
    /**
728
     * Creates a new model.
729
     *
730
     * @param array $data optional key-value properties to set
731
     *
732
     * @return bool true when the operation was successful
733
     *
734
     * @throws BadMethodCallException when called on an existing model
735
     */
736
    public function create(array $data = [])
737
    {
738
        if ($this->_id !== false) {
739
            throw new BadMethodCallException('Cannot call create() on an existing model');
740
        }
741
742
        // mass assign values passed into create()
743
        $this->setValues($data);
744
745
        // dispatch the model.creating event
746
        if (!$this->handleDispatch(ModelEvent::CREATING)) {
747
            return false;
748
        }
749
750
        $requiredProperties = [];
751
        foreach (static::$properties as $name => $property) {
752
            // build a list of the required properties
753
            if ($property['required']) {
754
                $requiredProperties[] = $name;
755
            }
756
757
            // add in default values
758
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
759
                $this->_unsaved[$name] = $property['default'];
760
            }
761
        }
762
763
        // validate the values being saved
764
        $validated = true;
765
        $insertArray = [];
766
        foreach ($this->_unsaved as $name => $value) {
767
            // exclude if value does not map to a property
768
            if (!isset(static::$properties[$name])) {
769
                continue;
770
            }
771
772
            $property = static::$properties[$name];
773
774
            // cannot insert immutable values
775
            // (unless using the default value)
776
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
777
                continue;
778
            }
779
780
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
781
            $insertArray[$name] = $value;
782
        }
783
784
        // check for required fields
785
        foreach ($requiredProperties as $name) {
786
            if (!isset($insertArray[$name])) {
787
                $property = static::$properties[$name];
788
                $this->getErrors()->push([
789
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
790
                    'params' => [
791
                        'field' => $name,
792
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
793
794
                $validated = false;
795
            }
796
        }
797
798
        if (!$validated) {
799
            return false;
800
        }
801
802
        $created = self::$driver->createModel($this, $insertArray);
803
804 View Code Duplication
        if ($created) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
805
            // determine the model's new ID
806
            $this->getNewID();
807
808
            // NOTE clear the local cache before the model.created
809
            // event so that fetching values forces a reload
810
            // from the storage layer
811
            $this->clearCache();
812
            $this->_persisted = true;
813
814
            // dispatch the model.created event
815
            if (!$this->handleDispatch(ModelEvent::CREATED)) {
816
                return false;
817
            }
818
        }
819
820
        return $created;
821
    }
822
823
    /**
824
     * Ignores unsaved values when fetching the next value.
825
     *
826
     * @return self
827
     */
828
    public function ignoreUnsaved()
829
    {
830
        $this->_ignoreUnsaved = true;
831
832
        return $this;
833
    }
834
835
    /**
836
     * Fetches property values from the model.
837
     *
838
     * This method looks up values in this order:
839
     * IDs, local cache, unsaved values, storage layer, defaults
840
     *
841
     * @param array $properties list of property names to fetch values of
842
     *
843
     * @return array
844
     */
845
    public function get(array $properties)
846
    {
847
        // load the values from the IDs and local model cache
848
        $values = array_replace($this->ids(), $this->_values);
849
850
        // unless specified, use any unsaved values
851
        $ignoreUnsaved = $this->_ignoreUnsaved;
852
        $this->_ignoreUnsaved = false;
853
854
        if (!$ignoreUnsaved) {
855
            $values = array_replace($values, $this->_unsaved);
856
        }
857
858
        // attempt to load any missing values from the storage layer
859
        $numMissing = count(array_diff($properties, array_keys($values)));
860
        if ($numMissing > 0) {
861
            $this->refresh();
862
            $values = array_replace($values, $this->_values);
863
864
            if (!$ignoreUnsaved) {
865
                $values = array_replace($values, $this->_unsaved);
866
            }
867
        }
868
869
        // build a key-value map of the requested properties
870
        $return = [];
871
        foreach ($properties as $k) {
872
            if (array_key_exists($k, $values)) {
873
                $return[$k] = $values[$k];
874
            // set any missing values to the default value
875
            } elseif (static::hasProperty($k)) {
876
                $return[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
877
            // use null for values of non-properties
878
            } else {
879
                $return[$k] = null;
880
            }
881
882
            // call any accessors
883
            if ($accessor = self::getAccessor($k)) {
884
                $return[$k] = $this->$accessor($return[$k]);
885
            }
886
        }
887
888
        return $return;
889
    }
890
891
    /**
892
     * Populates a newly created model with its ID.
893
     */
894
    protected function getNewID()
895
    {
896
        $ids = [];
897
        $namedIds = [];
898
        foreach (static::$ids as $k) {
899
            // attempt use the supplied value if the ID property is mutable
900
            $property = static::getProperty($k);
901
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
902
                $id = $this->_unsaved[$k];
903
            } else {
904
                $id = self::$driver->getCreatedID($this, $k);
905
            }
906
907
            $ids[] = $id;
908
            $namedIds[$k] = $id;
909
        }
910
911
        $this->_id = implode(',', $ids);
912
        $this->_ids = $namedIds;
913
    }
914
915
    /**
916
     * Sets a collection values on the model from an untrusted input.
917
     *
918
     * @param array $values
919
     *
920
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
921
     *
922
     * @return self
923
     */
924
    public function setValues($values)
925
    {
926
        // check if the model has a mass assignment whitelist
927
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
928
929
        // if no whitelist, then check for a blacklist
930
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
931
932
        foreach ($values as $k => $value) {
933
            // check for mass assignment violations
934
            if (($permitted && !in_array($k, $permitted)) ||
935
                ($protected && in_array($k, $protected))) {
936
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
937
            }
938
939
            $this->$k = $value;
940
        }
941
942
        return $this;
943
    }
944
945
    /**
946
     * Converts the model to an array.
947
     *
948
     * @return array
949
     */
950
    public function toArray()
951
    {
952
        // build the list of properties to retrieve
953
        $properties = array_keys(static::$properties);
954
955
        // remove any hidden properties
956
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
957
        $properties = array_diff($properties, $hide);
958
959
        // add any appended properties
960
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
961
        $properties = array_merge($properties, $append);
962
963
        // get the values for the properties
964
        $result = $this->get($properties);
965
966
        foreach ($result as $k => &$value) {
967
            // convert any models to arrays
968
            if ($value instanceof self) {
969
                $value = $value->toArray();
970
            }
971
        }
972
973
        // DEPRECATED
974
        // apply the transformation hook
975
        if (method_exists($this, 'toArrayHook')) {
976
            $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...
977
        }
978
979
        return $result;
980
    }
981
982
    /**
983
     * Updates the model.
984
     *
985
     * @param array $data optional key-value properties to set
986
     *
987
     * @return bool true when the operation was successful
988
     *
989
     * @throws BadMethodCallException when not called on an existing model
990
     */
991
    public function set(array $data = [])
992
    {
993
        if ($this->_id === false) {
994
            throw new BadMethodCallException('Can only call set() on an existing model');
995
        }
996
997
        // mass assign values passed into set()
998
        $this->setValues($data);
999
1000
        // not updating anything?
1001
        if (count($this->_unsaved) == 0) {
1002
            return true;
1003
        }
1004
1005
        // dispatch the model.updating event
1006
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
1007
            return false;
1008
        }
1009
1010
        // DEPRECATED
1011
        if (method_exists($this, 'preSetHook') && !$this->preSetHook($this->_unsaved)) {
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...
1012
            return false;
1013
        }
1014
1015
        // validate the values being saved
1016
        $validated = true;
1017
        $updateArray = [];
1018
        foreach ($this->_unsaved as $name => $value) {
1019
            // exclude if value does not map to a property
1020
            if (!isset(static::$properties[$name])) {
1021
                continue;
1022
            }
1023
1024
            $property = static::$properties[$name];
1025
1026
            // can only modify mutable properties
1027
            if ($property['mutable'] != self::MUTABLE) {
1028
                continue;
1029
            }
1030
1031
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
1032
            $updateArray[$name] = $value;
1033
        }
1034
1035
        if (!$validated) {
1036
            return false;
1037
        }
1038
1039
        $updated = self::$driver->updateModel($this, $updateArray);
1040
1041 View Code Duplication
        if ($updated) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1042
            // NOTE clear the local cache before the model.updated
1043
            // event so that fetching values forces a reload
1044
            // from the storage layer
1045
            $this->clearCache();
1046
            $this->_persisted = true;
1047
1048
            // dispatch the model.updated event
1049
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
1050
                return false;
1051
            }
1052
        }
1053
1054
        return $updated;
1055
    }
1056
1057
    /**
1058
     * Delete the model.
1059
     *
1060
     * @return bool true when the operation was successful
1061
     */
1062
    public function delete()
1063
    {
1064
        if ($this->_id === false) {
1065
            throw new BadMethodCallException('Can only call delete() on an existing model');
1066
        }
1067
1068
        // dispatch the model.deleting event
1069
        if (!$this->handleDispatch(ModelEvent::DELETING)) {
1070
            return false;
1071
        }
1072
1073
        // perform a hard (default) or soft delete
1074
        $hardDelete = true;
1075
        if (property_exists($this, 'softDelete')) {
1076
            $t = time();
1077
            $this->deleted_at = $t;
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1078
            $t = $this->filterAndValidate(static::getProperty('deleted_at'), 'deleted_at', $t);
0 ignored issues
show
Bug introduced by
It seems like static::getProperty('deleted_at') targeting Pulsar\Model::getProperty() can also be of type null; however, Pulsar\Model::filterAndValidate() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1079
            $deleted = self::$driver->updateModel($this, ['deleted_at' => $t]);
1080
            $hardDelete = false;
1081
        } else {
1082
            $deleted = self::$driver->deleteModel($this);
1083
        }
1084
1085
        if ($deleted) {
1086
            // dispatch the model.deleted event
1087
            if (!$this->handleDispatch(ModelEvent::DELETED)) {
1088
                return false;
1089
            }
1090
1091
            if ($hardDelete) {
1092
                $this->_persisted = false;
1093
            }
1094
        }
1095
1096
        return $deleted;
1097
    }
1098
1099
    /**
1100
     * Restores a soft-deleted model.
1101
     *
1102
     * @return bool
1103
     */
1104
    public function restore()
1105
    {
1106
        if (!property_exists($this, 'softDelete') || !$this->deleted_at) {
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1107
            throw new BadMethodCallException('Can only call restore() on a soft-deleted model');
1108
        }
1109
1110
        // dispatch the model.updating event
1111
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
1112
            return false;
1113
        }
1114
1115
        $this->deleted_at = null;
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1116
        $restored = self::$driver->updateModel($this, ['deleted_at' => null]);
1117
1118
        if ($restored) {
1119
            // dispatch the model.updated event
1120
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
1121
                return false;
1122
            }
1123
        }
1124
1125
        return $restored;
1126
    }
1127
1128
    /**
1129
     * Checks if the model has been deleted.
1130
     *
1131
     * @return bool
1132
     */
1133
    public function isDeleted()
1134
    {
1135
        if (property_exists($this, 'softDelete') && $this->deleted_at) {
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1136
            return true;
1137
        }
1138
1139
        return !$this->_persisted;
1140
    }
1141
1142
    /////////////////////////////
1143
    // Queries
1144
    /////////////////////////////
1145
1146
    /**
1147
     * Generates a new query instance.
1148
     *
1149
     * @return Query
1150
     */
1151
    public static function query()
1152
    {
1153
        // Create a new model instance for the query to ensure
1154
        // that the model's initialize() method gets called.
1155
        // Otherwise, the property definitions will be incomplete.
1156
        $model = new static();
1157
1158
        return new Query($model);
1159
    }
1160
1161
    /**
1162
     * Finds a single instance of a model given it's ID.
1163
     *
1164
     * @param mixed $id
1165
     *
1166
     * @return Model|null
1167
     */
1168
    public static function find($id)
1169
    {
1170
        $ids = [];
1171
        $id = (array) $id;
1172
        foreach (static::$ids as $j => $k) {
1173
            $ids[$k] = $id[$j];
1174
        }
1175
1176
        return static::where($ids)->first();
1177
    }
1178
1179
    /**
1180
     * Finds a single instance of a model given it's ID or throws an exception.
1181
     *
1182
     * @param mixed $id
1183
     *
1184
     * @return Model
1185
     *
1186
     * @throws ModelNotFoundException when a model could not be found
1187
     */
1188
    public static function findOrFail($id)
1189
    {
1190
        $model = static::find($id);
1191
        if (!$model) {
1192
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1193
        }
1194
1195
        return $model;
1196
    }
1197
1198
    /**
1199
     * @deprecated
1200
     *
1201
     * Checks if the model exists in the database
1202
     *
1203
     * @return bool
1204
     */
1205
    public function exists()
1206
    {
1207
        return static::where($this->ids())->count() == 1;
1208
    }
1209
1210
    /**
1211
     * Tells if this model instance has been persisted to the data layer.
1212
     *
1213
     * NOTE: this does not actually perform a check with the data layer
1214
     *
1215
     * @return bool
1216
     */
1217
    public function persisted()
1218
    {
1219
        return $this->_persisted;
1220
    }
1221
1222
    /**
1223
     * Loads the model from the storage layer.
1224
     *
1225
     * @return self
1226
     */
1227
    public function refresh()
1228
    {
1229
        if ($this->_id === false) {
1230
            return $this;
1231
        }
1232
1233
        $values = self::$driver->loadModel($this);
1234
1235
        if (!is_array($values)) {
1236
            return $this;
1237
        }
1238
1239
        // clear any relations
1240
        $this->_relationships = [];
1241
1242
        return $this->refreshWith($values);
1243
    }
1244
1245
    /**
1246
     * Loads values into the model.
1247
     *
1248
     * @param array $values values
1249
     *
1250
     * @return self
1251
     */
1252
    public function refreshWith(array $values)
1253
    {
1254
        $this->_persisted = true;
1255
        $this->_values = $values;
1256
1257
        return $this;
1258
    }
1259
1260
    /**
1261
     * Clears the cache for this model.
1262
     *
1263
     * @return self
1264
     */
1265
    public function clearCache()
1266
    {
1267
        $this->_unsaved = [];
1268
        $this->_values = [];
1269
        $this->_relationships = [];
1270
1271
        return $this;
1272
    }
1273
1274
    /////////////////////////////
1275
    // Relationships
1276
    /////////////////////////////
1277
1278
    /**
1279
     * @deprecated
1280
     *
1281
     * Gets the model for a has-one relationship
1282
     *
1283
     * @param string $k property
1284
     *
1285
     * @return Model|null
1286
     */
1287
    public function relation($k)
1288
    {
1289
        $id = $this->$k;
1290
        if (!$id) {
1291
            return;
1292
        }
1293
1294
        if (!isset($this->_relationships[$k])) {
1295
            $property = static::getProperty($k);
1296
            $relationModelClass = $property['relation'];
1297
            $this->_relationships[$k] = $relationModelClass::find($id);
1298
        }
1299
1300
        return $this->_relationships[$k];
1301
    }
1302
1303
    /**
1304
     * @deprecated
1305
     *
1306
     * Sets the model for a has-one relationship
1307
     *
1308
     * @param string $k
1309
     * @param Model  $model
1310
     *
1311
     * @return self
1312
     */
1313
    public function setRelation($k, Model $model)
1314
    {
1315
        $this->$k = $model->id();
1316
        $this->_relationships[$k] = $model;
1317
1318
        return $this;
1319
    }
1320
1321
    /**
1322
     * Creates the parent side of a One-To-One relationship.
1323
     *
1324
     * @param string $model      foreign model class
1325
     * @param string $foreignKey identifying key on foreign model
1326
     * @param string $localKey   identifying key on local model
1327
     *
1328
     * @return Relation\Relation
1329
     */
1330
    public function hasOne($model, $foreignKey = '', $localKey = '')
1331
    {
1332
        return new HasOne($this, $localKey, $model, $foreignKey);
1333
    }
1334
1335
    /**
1336
     * Creates the child side of a One-To-One or One-To-Many relationship.
1337
     *
1338
     * @param string $model      foreign model class
1339
     * @param string $foreignKey identifying key on foreign model
1340
     * @param string $localKey   identifying key on local model
1341
     *
1342
     * @return Relation\Relation
1343
     */
1344
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1345
    {
1346
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1347
    }
1348
1349
    /**
1350
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1351
     *
1352
     * @param string $model      foreign model class
1353
     * @param string $foreignKey identifying key on foreign model
1354
     * @param string $localKey   identifying key on local model
1355
     *
1356
     * @return Relation\Relation
1357
     */
1358
    public function hasMany($model, $foreignKey = '', $localKey = '')
1359
    {
1360
        return new HasMany($this, $localKey, $model, $foreignKey);
1361
    }
1362
1363
    /**
1364
     * Creates the child side of a Many-To-Many relationship.
1365
     *
1366
     * @param string $model      foreign model class
1367
     * @param string $tablename  pivot table name
1368
     * @param string $foreignKey identifying key on foreign model
1369
     * @param string $localKey   identifying key on local model
1370
     *
1371
     * @return \Pulsar\Relation\Relation
1372
     */
1373
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1374
    {
1375
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1376
    }
1377
1378
    /////////////////////////////
1379
    // Events
1380
    /////////////////////////////
1381
1382
    /**
1383
     * Gets the event dispatcher.
1384
     *
1385
     * @return EventDispatcher
1386
     */
1387
    public static function getDispatcher($ignoreCache = false)
1388
    {
1389
        $class = get_called_class();
1390
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1391
            self::$dispatchers[$class] = new EventDispatcher();
1392
        }
1393
1394
        return self::$dispatchers[$class];
1395
    }
1396
1397
    /**
1398
     * Subscribes to a listener to an event.
1399
     *
1400
     * @param string   $event    event name
1401
     * @param callable $listener
1402
     * @param int      $priority optional priority, higher #s get called first
1403
     */
1404
    public static function listen($event, callable $listener, $priority = 0)
1405
    {
1406
        static::getDispatcher()->addListener($event, $listener, $priority);
1407
    }
1408
1409
    /**
1410
     * Adds a listener to the model.creating and model.updating events.
1411
     *
1412
     * @param callable $listener
1413
     * @param int      $priority
1414
     */
1415
    public static function saving(callable $listener, $priority = 0)
1416
    {
1417
        static::listen(ModelEvent::CREATING, $listener, $priority);
1418
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1419
    }
1420
1421
    /**
1422
     * Adds a listener to the model.created and model.updated events.
1423
     *
1424
     * @param callable $listener
1425
     * @param int      $priority
1426
     */
1427
    public static function saved(callable $listener, $priority = 0)
1428
    {
1429
        static::listen(ModelEvent::CREATED, $listener, $priority);
1430
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1431
    }
1432
1433
    /**
1434
     * Adds a listener to the model.creating event.
1435
     *
1436
     * @param callable $listener
1437
     * @param int      $priority
1438
     */
1439
    public static function creating(callable $listener, $priority = 0)
1440
    {
1441
        static::listen(ModelEvent::CREATING, $listener, $priority);
1442
    }
1443
1444
    /**
1445
     * Adds a listener to the model.created event.
1446
     *
1447
     * @param callable $listener
1448
     * @param int      $priority
1449
     */
1450
    public static function created(callable $listener, $priority = 0)
1451
    {
1452
        static::listen(ModelEvent::CREATED, $listener, $priority);
1453
    }
1454
1455
    /**
1456
     * Adds a listener to the model.updating event.
1457
     *
1458
     * @param callable $listener
1459
     * @param int      $priority
1460
     */
1461
    public static function updating(callable $listener, $priority = 0)
1462
    {
1463
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1464
    }
1465
1466
    /**
1467
     * Adds a listener to the model.updated event.
1468
     *
1469
     * @param callable $listener
1470
     * @param int      $priority
1471
     */
1472
    public static function updated(callable $listener, $priority = 0)
1473
    {
1474
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1475
    }
1476
1477
    /**
1478
     * Adds a listener to the model.deleting event.
1479
     *
1480
     * @param callable $listener
1481
     * @param int      $priority
1482
     */
1483
    public static function deleting(callable $listener, $priority = 0)
1484
    {
1485
        static::listen(ModelEvent::DELETING, $listener, $priority);
1486
    }
1487
1488
    /**
1489
     * Adds a listener to the model.deleted event.
1490
     *
1491
     * @param callable $listener
1492
     * @param int      $priority
1493
     */
1494
    public static function deleted(callable $listener, $priority = 0)
1495
    {
1496
        static::listen(ModelEvent::DELETED, $listener, $priority);
1497
    }
1498
1499
    /**
1500
     * Dispatches an event.
1501
     *
1502
     * @param string $eventName
1503
     *
1504
     * @return ModelEvent
1505
     */
1506
    protected function dispatch($eventName)
1507
    {
1508
        $event = new ModelEvent($this);
1509
1510
        return static::getDispatcher()->dispatch($eventName, $event);
1511
    }
1512
1513
    /**
1514
     * Dispatches the given event and checks if it was successful.
1515
     *
1516
     * @param string $eventName
1517
     *
1518
     * @return bool true if the events were successfully propagated
1519
     */
1520
    private function handleDispatch($eventName)
1521
    {
1522
        $event = $this->dispatch($eventName);
1523
1524
        return !$event->isPropagationStopped();
1525
    }
1526
1527
    /////////////////////////////
1528
    // Validation
1529
    /////////////////////////////
1530
1531
    /**
1532
     * Gets the error stack for this model.
1533
     *
1534
     * @return ErrorStack
1535
     */
1536
    public function getErrors()
1537
    {
1538
        if (!$this->_errors && self::$globalErrorStack) {
1539
            $this->_errors = self::$globalErrorStack;
1540
        } elseif (!$this->_errors) {
1541
            $this->_errors = new ErrorStack();
1542
        }
1543
1544
        return $this->_errors;
1545
    }
1546
1547
    /**
1548
     * Validates and marshals a value to storage.
1549
     *
1550
     * @param array  $property
1551
     * @param string $propertyName
1552
     * @param mixed  $value
1553
     *
1554
     * @return bool
1555
     */
1556
    private function filterAndValidate(array $property, $propertyName, &$value)
1557
    {
1558
        // assume empty string is a null value for properties
1559
        // that are marked as optionally-null
1560
        if ($property['null'] && empty($value)) {
1561
            $value = null;
1562
1563
            return true;
1564
        }
1565
1566
        // validate
1567
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1568
1569
        // unique?
1570
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1571
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1572
        }
1573
1574
        return $valid;
1575
    }
1576
1577
    /**
1578
     * Validates a value for a property.
1579
     *
1580
     * @param array  $property
1581
     * @param string $propertyName
1582
     * @param mixed  $value
1583
     *
1584
     * @return array
1585
     */
1586
    private function validate(array $property, $propertyName, $value)
1587
    {
1588
        $valid = true;
1589
1590
        if (isset($property['validate']) && is_callable($property['validate'])) {
1591
            $valid = call_user_func_array($property['validate'], [$value]);
1592
        } elseif (isset($property['validate'])) {
1593
            $valid = Validate::is($value, $property['validate']);
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Validate::is() has been deprecated with message: Validates one or more fields based upon certain filters. Filters may be chained and will be executed in order
i.e. Validate::is( '[email protected]', 'email' ) or Validate::is( ['password1', 'password2'], 'matching|password:8|required' ). NOTE: some filters may modify the data, which is passed in by reference

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

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

Loading history...
1594
        }
1595
1596 View Code Duplication
        if (!$valid) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1597
            $this->getErrors()->push([
1598
                'error' => self::ERROR_VALIDATION_FAILED,
1599
                'params' => [
1600
                    'field' => $propertyName,
1601
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1602
        }
1603
1604
        return [$valid, $value];
1605
    }
1606
1607
    /**
1608
     * Checks if a value is unique for a property.
1609
     *
1610
     * @param array  $property
1611
     * @param string $propertyName
1612
     * @param mixed  $value
1613
     *
1614
     * @return bool
1615
     */
1616
    private function checkUniqueness(array $property, $propertyName, $value)
1617
    {
1618
        $n = static::where([$propertyName => $value])->count();
1619 View Code Duplication
        if ($n > 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1620
            $this->getErrors()->push([
1621
                'error' => self::ERROR_NOT_UNIQUE,
1622
                'params' => [
1623
                    'field' => $propertyName,
1624
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1625
1626
            return false;
1627
        }
1628
1629
        return true;
1630
    }
1631
1632
    /**
1633
     * Gets the marshaled default value for a property (if set).
1634
     *
1635
     * @param string $property
1636
     *
1637
     * @return mixed
1638
     */
1639
    private function getPropertyDefault(array $property)
1640
    {
1641
        return array_value($property, 'default');
1642
    }
1643
}
1644