Completed
Push — master ( 2a5f2d...1bf18c )
by Jared
01:42 queued 10s
created

Model::getMassAssignmentWhitelist()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
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 ArrayAccess;
15
use BadMethodCallException;
16
use ICanBoogie\Inflector;
17
use InvalidArgumentException;
18
use Pulsar\Driver\DriverInterface;
19
use Pulsar\Event\AbstractEvent;
20
use Pulsar\Event\ModelCreated;
21
use Pulsar\Event\ModelCreating;
22
use Pulsar\Event\ModelDeleted;
23
use Pulsar\Event\ModelDeleting;
24
use Pulsar\Event\ModelUpdated;
25
use Pulsar\Event\ModelUpdating;
26
use Pulsar\Exception\DriverMissingException;
27
use Pulsar\Exception\MassAssignmentException;
28
use Pulsar\Exception\ModelException;
29
use Pulsar\Exception\ModelNotFoundException;
30
use Pulsar\Relation\AbstractRelation;
31
use Pulsar\Relation\Relationship;
32
use Symfony\Component\EventDispatcher\EventDispatcher;
33
34
/**
35
 * Class Model.
36
 *
37
 * @method Query             where($where, $value = null, $condition = null)
38
 * @method Query             limit($limit)
39
 * @method Query             start($start)
40
 * @method Query             sort($sort)
41
 * @method Query             join($model, $column, $foreignKey)
42
 * @method Query             with($k)
43
 * @method Iterator          all()
44
 * @method array|static|null first($limit = 1)
45
 * @method int               count()
46
 * @method number            sum($property)
47
 * @method number            average($property)
48
 * @method number            max($property)
49
 * @method number            min($property)
50
 */
51
abstract class Model implements ArrayAccess
52
{
53
    const DEFAULT_ID_NAME = 'id';
54
55
    /////////////////////////////
56
    // Model visible variables
57
    /////////////////////////////
58
59
    /**
60
     * List of model ID property names.
61
     *
62
     * @var array
63
     */
64
    protected static $ids = [self::DEFAULT_ID_NAME];
65
66
    /**
67
     * Property definitions expressed as a key-value map with
68
     * property names as the keys.
69
     * i.e. ['enabled' => ['type' => Type::BOOLEAN]].
70
     *
71
     * @var array
72
     */
73
    protected static $properties = [];
74
75
    /**
76
     * @var array
77
     */
78
    protected $_values = [];
79
80
    /**
81
     * @var array
82
     */
83
    private $_unsaved = [];
84
85
    /**
86
     * @var bool
87
     */
88
    protected $_persisted = false;
89
90
    /**
91
     * @var array
92
     */
93
    protected $_relationships = [];
94
95
    /**
96
     * @var AbstractRelation[]
97
     */
98
    private $relationships = [];
99
100
    /////////////////////////////
101
    // Base model variables
102
    /////////////////////////////
103
104
    /**
105
     * @var array
106
     */
107
    private static $initialized = [];
108
109
    /**
110
     * @var DriverInterface
111
     */
112
    private static $driver;
113
114
    /**
115
     * @var array
116
     */
117
    private static $accessors = [];
118
119
    /**
120
     * @var array
121
     */
122
    private static $mutators = [];
123
124
    /**
125
     * @var array
126
     */
127
    private static $dispatchers = [];
128
129
    /**
130
     * @var string
131
     */
132
    private $tablename;
133
134
    /**
135
     * @var bool
136
     */
137
    private $hasId;
138
139
    /**
140
     * @var array
141
     */
142
    private $idValues;
143
144
    /**
145
     * @var bool
146
     */
147
    private $loaded = false;
148
149
    /**
150
     * @var Errors
151
     */
152
    private $errors;
153
154
    /**
155
     * @var bool
156
     */
157
    private $ignoreUnsaved;
158
159
    /**
160
     * Creates a new model object.
161
     *
162
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
0 ignored issues
show
Bug introduced by
There is no parameter named $id. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
163
     * @param array                    $values optional key-value map to pre-seed model
164
     */
165
    public function __construct(array $values = [])
166
    {
167
        // initialize the model
168
        $this->init();
169
170
        $ids = [];
171
        $this->hasId = true;
172
        foreach (static::$ids as $name) {
173
            $id = false;
174
            if (array_key_exists($name, $values)) {
175
                $idProperty = static::definition()->get($name);
176
                $id = Type::cast($idProperty, $values[$name]);
0 ignored issues
show
Bug introduced by
It seems like $idProperty defined by static::definition()->get($name) on line 175 can be null; however, Pulsar\Type::cast() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
177
            }
178
179
            $ids[$name] = $id;
180
            $this->hasId = $this->hasId && $id;
181
        }
182
183
        $this->idValues = $ids;
184
185
        // load any given values
186
        if ($this->hasId && count($values) > count($ids)) {
187
            $this->refreshWith($values);
188
        } elseif (!$this->hasId) {
189
            $this->_unsaved = $values;
190
        }
191
    }
192
193
    /**
194
     * Performs initialization on this model.
195
     */
196
    private function init()
197
    {
198
        // ensure the initialize function is called only once
199
        $k = static::class;
200
        if (!isset(self::$initialized[$k])) {
201
            $this->initialize();
202
            self::$initialized[$k] = true;
203
        }
204
    }
205
206
    /**
207
     * The initialize() method is called once per model. This is a great
208
     * place to install event listeners. Any methods on the model that have
209
     * "autoInitialize" in the name will automatically be called.
210
     */
211
    protected function initialize()
212
    {
213
        // Use reflection to automatically call any method here that has a name
214
        // that starts with "autoInitialize". This is useful for traits to install listeners.
215
        $methods = get_class_methods(static::class);
216
        foreach ($methods as $method) {
217
            if (0 === strpos($method, 'autoInitialize')) {
218
                $this->$method();
219
            }
220
        }
221
    }
222
223
    /**
224
     * Sets the driver for all models.
225
     */
226
    public static function setDriver(DriverInterface $driver)
227
    {
228
        self::$driver = $driver;
229
    }
230
231
    /**
232
     * Gets the driver for all models.
233
     *
234
     * @throws DriverMissingException when a driver has not been set yet
235
     */
236
    public static function getDriver(): DriverInterface
237
    {
238
        if (!self::$driver) {
239
            throw new DriverMissingException('A model driver has not been set yet.');
240
        }
241
242
        return self::$driver;
243
    }
244
245
    /**
246
     * Clears the driver for all models.
247
     */
248
    public static function clearDriver()
249
    {
250
        self::$driver = null;
251
    }
252
253
    /**
254
     * Gets the name of the model, i.e. User.
255
     */
256
    public static function modelName(): string
257
    {
258
        // strip namespacing
259
        $paths = explode('\\', static::class);
260
261
        return end($paths);
262
    }
263
264
    /**
265
     * Gets the model ID.
266
     *
267
     * @return string|number|false ID
268
     */
269
    public function id()
270
    {
271
        if (!$this->hasId) {
272
            return false;
273
        }
274
275
        if (1 == count($this->idValues)) {
276
            return reset($this->idValues);
277
        }
278
279
        $result = [];
280
        foreach (static::$ids as $k) {
281
            $result[] = $this->idValues[$k];
282
        }
283
284
        return implode(',', $result);
285
    }
286
287
    /**
288
     * Gets a key-value map of the model ID.
289
     *
290
     * @return array ID map
291
     */
292
    public function ids(): array
293
    {
294
        return $this->idValues;
295
    }
296
297
    /**
298
     * Checks if the model has an identifier present.
299
     * This does not indicate whether the model has been
300
     * persisted to the database or loaded from the database.
301
     */
302
    public function hasId(): bool
303
    {
304
        return $this->hasId;
305
    }
306
307
    /////////////////////////////
308
    // Magic Methods
309
    /////////////////////////////
310
311
    /**
312
     * Converts the model into a string.
313
     *
314
     * @return string
315
     */
316
    public function __toString()
317
    {
318
        $values = array_merge($this->_values, $this->_unsaved, $this->idValues);
319
        ksort($values);
320
321
        return static::class.'('.json_encode($values, JSON_PRETTY_PRINT).')';
322
    }
323
324
    /**
325
     * Shortcut to a get() call for a given property.
326
     *
327
     * @param string $name
328
     *
329
     * @return mixed
330
     */
331
    public function __get($name)
332
    {
333
        $result = $this->get([$name]);
334
335
        return reset($result);
336
    }
337
338
    /**
339
     * Sets an unsaved value.
340
     *
341
     * @param string $name
342
     * @param mixed  $value
343
     */
344
    public function __set($name, $value)
345
    {
346
        // if changing property, remove relation model
347
        if (isset($this->_relationships[$name])) {
348
            unset($this->_relationships[$name]);
349
        }
350
351
        // call any mutators
352
        $mutator = self::getMutator($name);
353
        if ($mutator) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mutator of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
354
            $this->_unsaved[$name] = $this->$mutator($value);
355
        } else {
356
            $this->_unsaved[$name] = $value;
357
        }
358
359
        // set local ID property on belongs_to relationship
360
        if (static::definition()->has($name)) {
361
            $property = static::definition()->get($name);
362
            if (Relationship::BELONGS_TO == $property->getRelationshipType() && !$property->isPersisted()) {
363
                if ($value instanceof self) {
364
                    $this->_unsaved[$property->getLocalKey()] = $value->{$property->getForeignKey()};
365
                } elseif (null === $value) {
366
                    $this->_unsaved[$property->getLocalKey()] = null;
367
                } else {
368
                    throw new ModelException('The value set on the "'.$name.'" property must be a model or null.');
369
                }
370
            }
371
        }
372
    }
373
374
    /**
375
     * Checks if an unsaved value or property exists by this name.
376
     *
377
     * @param string $name
378
     *
379
     * @return bool
380
     */
381
    public function __isset($name)
382
    {
383
        // isset() must return true for any value that could be returned by offsetGet
384
        // because many callers will first check isset() to see if the value is accessible.
385
        // This method is not supposed to only be valid for unsaved values, or properties
386
        // that have a value.
387
        return array_key_exists($name, $this->_unsaved) || static::definition()->has($name);
388
    }
389
390
    /**
391
     * Unsets an unsaved value.
392
     *
393
     * @param string $name
394
     */
395
    public function __unset($name)
396
    {
397
        if (array_key_exists($name, $this->_unsaved)) {
398
            // if changing property, remove relation model
399
            if (isset($this->_relationships[$name])) {
400
                unset($this->_relationships[$name]);
401
            }
402
403
            unset($this->_unsaved[$name]);
404
        }
405
    }
406
407
    /////////////////////////////
408
    // ArrayAccess Interface
409
    /////////////////////////////
410
411
    public function offsetExists($offset)
412
    {
413
        return isset($this->$offset);
414
    }
415
416
    public function offsetGet($offset)
417
    {
418
        return $this->$offset;
419
    }
420
421
    public function offsetSet($offset, $value)
422
    {
423
        $this->$offset = $value;
424
    }
425
426
    public function offsetUnset($offset)
427
    {
428
        unset($this->$offset);
429
    }
430
431
    public static function __callStatic($name, $parameters)
432
    {
433
        // Any calls to unkown static methods should be deferred to
434
        // the query. This allows calls like User::where()
435
        // to replace User::query()->where().
436
        return call_user_func_array([static::query(), $name], $parameters);
437
    }
438
439
    /////////////////////////////
440
    // Property Definitions
441
    /////////////////////////////
442
443
    /**
444
     * Gets the model definition.
445
     */
446
    public static function definition(): Definition
447
    {
448
        return DefinitionBuilder::get(static::class);
449
    }
450
451
    /**
452
     * The buildDefinition() method is called once per model. It's used
453
     * to generate the model definition. This is a great place to add any
454
     * dynamic model properties.
455
     */
456
    public static function buildDefinition(): Definition
457
    {
458
        // Use reflection to automatically call any method on the model that has a name
459
        // that starts with "buildDefinition". This is useful for traits to add properties.
460
        $methods = get_class_methods(static::class);
461
        foreach ($methods as $method) {
462
            if (0 === strpos($method, 'autoDefinition')) {
463
                static::$method();
464
            }
465
        }
466
467
        return DefinitionBuilder::build(static::$properties, static::class);
468
    }
469
470
    /**
471
     * Gets the names of the model ID properties.
472
     */
473
    public static function getIDProperties(): array
474
    {
475
        return static::$ids;
476
    }
477
478
    /**
479
     * Gets the mutator method name for a given property name.
480
     * Looks for methods in the form of `setPropertyValue`.
481
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
482
     *
483
     * @param string $property property
484
     *
485
     * @return string|null method name if it exists
486
     */
487
    public static function getMutator(string $property): ?string
488
    {
489
        $class = static::class;
490
491
        $k = $class.':'.$property;
492
        if (!array_key_exists($k, self::$mutators)) {
493
            $inflector = Inflector::get();
494
            $method = 'set'.$inflector->camelize($property).'Value';
495
496
            if (!method_exists($class, $method)) {
497
                $method = null;
498
            }
499
500
            self::$mutators[$k] = $method;
501
        }
502
503
        return self::$mutators[$k];
504
    }
505
506
    /**
507
     * Gets the accessor method name for a given property name.
508
     * Looks for methods in the form of `getPropertyValue`.
509
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
510
     *
511
     * @param string $property property
512
     *
513
     * @return string|null method name if it exists
514
     */
515
    public static function getAccessor(string $property): ?string
516
    {
517
        $class = static::class;
518
519
        $k = $class.':'.$property;
520
        if (!array_key_exists($k, self::$accessors)) {
521
            $inflector = Inflector::get();
522
            $method = 'get'.$inflector->camelize($property).'Value';
523
524
            if (!method_exists($class, $method)) {
525
                $method = null;
526
            }
527
528
            self::$accessors[$k] = $method;
529
        }
530
531
        return self::$accessors[$k];
532
    }
533
534
    /////////////////////////////
535
    // CRUD Operations
536
    /////////////////////////////
537
538
    /**
539
     * Gets the table name for storing this model.
540
     */
541
    public function getTablename(): string
542
    {
543
        if (!$this->tablename) {
544
            $inflector = Inflector::get();
545
546
            $this->tablename = $inflector->camelize($inflector->pluralize(static::modelName()));
547
        }
548
549
        return $this->tablename;
550
    }
551
552
    /**
553
     * Gets the ID of the connection in the connection manager
554
     * that stores this model.
555
     */
556
    public function getConnection(): ?string
557
    {
558
        return null;
559
    }
560
561
    protected function usesTransactions(): bool
562
    {
563
        return false;
564
    }
565
566
    /**
567
     * Saves the model.
568
     *
569
     * @return bool true when the operation was successful
570
     */
571
    public function save(): bool
572
    {
573
        if (!$this->hasId) {
574
            return $this->create();
575
        }
576
577
        return $this->set();
578
    }
579
580
    /**
581
     * Saves the model. Throws an exception when the operation fails.
582
     *
583
     * @throws ModelException when the model cannot be saved
584
     */
585
    public function saveOrFail()
586
    {
587
        if (!$this->save()) {
588
            $msg = 'Failed to save '.static::modelName();
589
            if ($validationErrors = $this->getErrors()->all()) {
590
                $msg .= ': '.implode(', ', $validationErrors);
591
            }
592
593
            throw new ModelException($msg);
594
        }
595
    }
596
597
    /**
598
     * Creates a new model.
599
     *
600
     * @param array $data optional key-value properties to set
601
     *
602
     * @return bool true when the operation was successful
603
     *
604
     * @throws BadMethodCallException when called on an existing model
605
     */
606
    public function create(array $data = []): bool
607
    {
608
        if ($this->hasId) {
609
            throw new BadMethodCallException('Cannot call create() on an existing model');
610
        }
611
612
        // mass assign values passed into create()
613
        $this->setValues($data);
614
615
        // clear any previous errors
616
        $this->getErrors()->clear();
617
618
        // start a DB transaction if needed
619
        $usesTransactions = $this->usesTransactions();
620
        if ($usesTransactions) {
621
            self::$driver->startTransaction($this->getConnection());
622
        }
623
624
        // dispatch the model.creating event
625
        if (!$this->performDispatch(new ModelCreating($this), $usesTransactions)) {
626
            return false;
627
        }
628
629
        $requiredProperties = [];
630
        foreach (static::definition()->all() as $name => $property) {
631
            // build a list of the required properties
632
            if ($property->isRequired()) {
633
                $requiredProperties[] = $property;
634
            }
635
636
            // add in default values
637
            if (!array_key_exists($name, $this->_unsaved) && $property->hasDefault()) {
638
                $this->_unsaved[$name] = $property->getDefault();
639
            }
640
        }
641
642
        // save any relationships
643
        if (!$this->saveRelationships($usesTransactions)) {
644
            return false;
645
        }
646
647
        // validate the values being saved
648
        $validated = true;
649
        $insertArray = [];
650
        $preservedValues = [];
651
        foreach ($this->_unsaved as $name => $value) {
652
            // exclude if value does not map to a property
653
            $property = static::definition()->get($name);
654
            if (!$property) {
655
                continue;
656
            }
657
658
            // check if this property is persisted to the DB
659
            if (!$property->isPersisted()) {
660
                $preservedValues[$name] = $value;
661
                continue;
662
            }
663
664
            // cannot insert immutable values
665
            // (unless using the default value)
666
            if ($property->isImmutable() && $value !== $property->getDefault()) {
667
                continue;
668
            }
669
670
            $validated = $validated && Validator::validateProperty($this, $property, $value);
671
            $insertArray[$name] = $value;
672
        }
673
674
        // check for required fields
675
        foreach ($requiredProperties as $property) {
676
            $name = $property->getName();
677
            if (!isset($insertArray[$name]) && !isset($preservedValues[$name])) {
678
                $context = [
679
                    'field' => $name,
680
                    'field_name' => $property->getTitle($this),
681
                ];
682
                $this->getErrors()->add('pulsar.validation.required', $context);
683
684
                $validated = false;
685
            }
686
        }
687
688
        if (!$validated) {
689
            // when validations fail roll back any database transaction
690
            if ($usesTransactions) {
691
                self::$driver->rollBackTransaction($this->getConnection());
692
            }
693
694
            return false;
695
        }
696
697
        $created = self::$driver->createModel($this, $insertArray);
698
699
        if ($created) {
700
            // determine the model's new ID
701
            $this->getNewId();
702
703
            // store the persisted values to the in-memory cache
704
            $this->_unsaved = [];
705
            $hydrateValues = array_replace($this->idValues, $preservedValues);
706
707
            // only type-cast the values that were converted to the database format
708
            foreach ($insertArray as $k => $v) {
709
                if ($property = static::definition()->get($k)) {
710
                    $hydrateValues[$k] = Type::cast($property, $v);
711
                } else {
712
                    $hydrateValues[$k] = $v;
713
                }
714
            }
715
            $this->refreshWith($hydrateValues);
716
717
            // dispatch the model.created event
718
            if (!$this->performDispatch(new ModelCreated($this), $usesTransactions)) {
719
                return false;
720
            }
721
        }
722
723
        // commit the transaction, if used
724
        if ($usesTransactions) {
725
            self::$driver->commitTransaction($this->getConnection());
726
        }
727
728
        return $created;
729
    }
730
731
    /**
732
     * Ignores unsaved values when fetching the next value.
733
     *
734
     * @return $this
735
     */
736
    public function ignoreUnsaved()
737
    {
738
        $this->ignoreUnsaved = true;
739
740
        return $this;
741
    }
742
743
    /**
744
     * Fetches property values from the model.
745
     *
746
     * This method looks up values in this order:
747
     * IDs, local cache, unsaved values, storage layer, defaults
748
     *
749
     * @param array $properties list of property names to fetch values of
750
     */
751
    public function get(array $properties): array
752
    {
753
        // load the values from the IDs and local model cache
754
        $values = array_replace($this->ids(), $this->_values);
755
756
        // unless specified, use any unsaved values
757
        $ignoreUnsaved = $this->ignoreUnsaved;
758
        $this->ignoreUnsaved = false;
759
760
        if (!$ignoreUnsaved) {
761
            $values = array_replace($values, $this->_unsaved);
762
        }
763
764
        // see if there are any model properties that do not exist.
765
        // when true then this means the model needs to be hydrated
766
        // NOTE: only looking at model properties and excluding dynamic/non-existent properties
767
        $modelProperties = static::definition()->propertyNames();
768
        $numMissing = count(array_intersect($modelProperties, array_diff($properties, array_keys($values))));
769
770
        if ($numMissing > 0 && !$this->loaded) {
771
            // load the model from the storage layer, if needed
772
            $this->refresh();
773
774
            $values = array_replace($values, $this->_values);
775
776
            if (!$ignoreUnsaved) {
777
                $values = array_replace($values, $this->_unsaved);
778
            }
779
        }
780
781
        // build a key-value map of the requested properties
782
        $return = [];
783
        foreach ($properties as $k) {
784
            $return[$k] = $this->getValue($k, $values);
785
        }
786
787
        return $return;
788
    }
789
790
    /**
791
     * Gets a property value from the model.
792
     *
793
     * Values are looked up in this order:
794
     *  1. unsaved values
795
     *  2. local values
796
     *  3. default value
797
     *  4. null
798
     *
799
     * @return mixed
800
     */
801
    private function getValue(string $name, array $values)
802
    {
803
        $value = null;
804
805
        if (array_key_exists($name, $values)) {
806
            $value = $values[$name];
807
        } elseif ($property = static::definition()->get($name)) {
808
            if ($property->getRelationshipType() && !$property->isPersisted()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $property->getRelationshipType() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
809
                $relationship = $this->getRelationship($property);
810
                $value = $this->_values[$name] = $relationship->getResults();
811
            } else {
812
                $value = $this->_values[$name] = $property->getDefault();
813
            }
814
        }
815
816
        // call any accessors
817
        if ($accessor = self::getAccessor($name)) {
818
            $value = $this->$accessor($value);
819
        }
820
821
        return $value;
822
    }
823
824
    /**
825
     * Populates a newly created model with its ID.
826
     */
827
    private function getNewId()
828
    {
829
        $ids = [];
830
        $namedIds = [];
831
        foreach (static::$ids as $k) {
832
            // attempt use the supplied value if the ID property is mutable
833
            $property = static::definition()->get($k);
834
            if (!$property->isImmutable() && isset($this->_unsaved[$k])) {
835
                $id = $this->_unsaved[$k];
836
            } else {
837
                // type-cast the value because it came from the database
838
                $id = Type::cast($property, self::$driver->getCreatedId($this, $k));
0 ignored issues
show
Bug introduced by
It seems like $property defined by static::definition()->get($k) on line 833 can be null; however, Pulsar\Type::cast() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
839
            }
840
841
            $ids[] = $id;
842
            $namedIds[$k] = $id;
843
        }
844
845
        $this->hasId = true;
846
        $this->idValues = $namedIds;
847
    }
848
849
    protected function getMassAssignmentWhitelist(): ?array
850
    {
851
        if (!property_exists($this, 'permitted')) {
852
            return null;
853
        }
854
855
        return static::$permitted;
856
    }
857
858
    protected function getMassAssignmentBlacklist(): ?array
859
    {
860
        if (!property_exists($this, 'protected')) {
861
            return null;
862
        }
863
864
        return static::$protected;
865
    }
866
867
    /**
868
     * Sets a collection values on the model from an untrusted input.
869
     *
870
     * @param array $values
871
     *
872
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
873
     *
874
     * @return $this
875
     */
876
    public function setValues($values)
877
    {
878
        if ($permitted = $this->getMassAssignmentWhitelist()) {
879
            // use a mass assignment whitelist
880
            foreach ($values as $k => $value) {
881
                // check for mass assignment violations
882
                if (!in_array($k, $permitted)) {
883
                    throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
884
                }
885
886
                $this->$k = $value;
887
            }
888
        } elseif ($protected = $this->getMassAssignmentBlacklist()) {
889
            // use a mass assignment blacklist
890
            foreach ($values as $k => $value) {
891
                // check for mass assignment violations
892
                if (in_array($k, $protected)) {
893
                    throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
894
                }
895
896
                $this->$k = $value;
897
            }
898
        } else {
899
            // no mass assignment protection enabled
900
            foreach ($values as $k => $value) {
901
                $this->$k = $value;
902
            }
903
        }
904
905
        return $this;
906
    }
907
908
    /**
909
     * Converts the model to an array.
910
     */
911
    public function toArray(): array
912
    {
913
        // build the list of properties to retrieve
914
        $properties = static::definition()->propertyNames();
915
916
        // remove any relationships
917
        $relationships = [];
918
        foreach (static::definition()->all() as $property) {
919
            if ($property->getRelationshipType() && !$property->isPersisted()) {
920
                $relationships[] = $property->getName();
921
            }
922
        }
923
        $properties = array_diff($properties, $relationships);
924
925
        // remove any hidden properties
926
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
927
        $properties = array_diff($properties, $hide);
928
929
        // add any appended properties
930
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
931
        $properties = array_merge($properties, $append);
932
933
        // get the values for the properties
934
        $result = $this->get($properties);
935
936
        foreach ($result as $k => &$value) {
937
            // convert arrays of models to arrays
938
            if (is_array($value)) {
939
                foreach ($value as &$subValue) {
940
                    if ($subValue instanceof Model) {
941
                        $subValue = $subValue->toArray();
942
                    }
943
                }
944
            }
945
946
            // convert any models to arrays
947
            if ($value instanceof self) {
948
                $value = $value->toArray();
949
            }
950
        }
951
952
        return $result;
953
    }
954
955
    /**
956
     * Checks if the unsaved value for a property is present and
957
     * is different from the original value.
958
     *
959
     * @property string|null $name
960
     * @property bool        $hasChanged when true, checks if the unsaved value is different from the saved value
961
     */
962
    public function dirty(?string $name = null, bool $hasChanged = false): bool
963
    {
964
        if (!$name) {
965
            if ($hasChanged) {
966
                throw new \RuntimeException('Checking if all properties have changed is not supported');
967
            }
968
969
            return count($this->_unsaved) > 0;
970
        }
971
972
        if (!array_key_exists($name, $this->_unsaved)) {
973
            return false;
974
        }
975
976
        if (!$hasChanged) {
977
            return true;
978
        }
979
980
        return $this->$name !== $this->ignoreUnsaved()->$name;
981
    }
982
983
    /**
984
     * Updates the model.
985
     *
986
     * @param array $data optional key-value properties to set
987
     *
988
     * @return bool true when the operation was successful
989
     *
990
     * @throws BadMethodCallException when not called on an existing model
991
     */
992
    public function set(array $data = []): bool
993
    {
994
        if (!$this->hasId) {
995
            throw new BadMethodCallException('Can only call set() on an existing model');
996
        }
997
998
        // mass assign values passed into set()
999
        $this->setValues($data);
1000
1001
        // clear any previous errors
1002
        $this->getErrors()->clear();
1003
1004
        // not updating anything?
1005
        if (0 == count($this->_unsaved)) {
1006
            return true;
1007
        }
1008
1009
        // start a DB transaction if needed
1010
        $usesTransactions = $this->usesTransactions();
1011
        if ($usesTransactions) {
1012
            self::$driver->startTransaction($this->getConnection());
1013
        }
1014
1015
        // dispatch the model.updating event
1016
        if (!$this->performDispatch(new ModelUpdating($this), $usesTransactions)) {
1017
            return false;
1018
        }
1019
1020
        // save any relationships
1021
        if (!$this->saveRelationships($usesTransactions)) {
1022
            return false;
1023
        }
1024
1025
        // validate the values being saved
1026
        $validated = true;
1027
        $updateArray = [];
1028
        $preservedValues = [];
1029
        foreach ($this->_unsaved as $name => $value) {
1030
            // exclude if value does not map to a property
1031
            if (!static::definition()->has($name)) {
1032
                continue;
1033
            }
1034
1035
            $property = static::definition()->get($name);
1036
1037
            // check if this property is persisted to the DB
1038
            if (!$property->isPersisted()) {
1039
                $preservedValues[$name] = $value;
1040
                continue;
1041
            }
1042
1043
            // can only modify mutable properties
1044
            if (!$property->isMutable()) {
1045
                continue;
1046
            }
1047
1048
            $validated = $validated && Validator::validateProperty($this, $property, $value);
0 ignored issues
show
Bug introduced by
It seems like $property defined by static::definition()->get($name) on line 1035 can be null; however, Pulsar\Validator::validateProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1049
            $updateArray[$name] = $value;
1050
        }
1051
1052
        if (!$validated) {
1053
            // when validations fail roll back any database transaction
1054
            if ($usesTransactions) {
1055
                self::$driver->rollBackTransaction($this->getConnection());
1056
            }
1057
1058
            return false;
1059
        }
1060
1061
        $updated = self::$driver->updateModel($this, $updateArray);
1062
1063
        if ($updated) {
1064
            // store the persisted values to the in-memory cache
1065
            $this->_unsaved = [];
1066
            $hydrateValues = array_replace($this->_values, $this->idValues, $preservedValues);
1067
1068
            // only type-cast the values that were converted to the database format
1069
            foreach ($updateArray as $k => $v) {
1070
                if ($property = static::definition()->get($k)) {
1071
                    $hydrateValues[$k] = Type::cast($property, $v);
1072
                } else {
1073
                    $hydrateValues[$k] = $v;
1074
                }
1075
            }
1076
            $this->refreshWith($hydrateValues);
1077
1078
            // dispatch the model.updated event
1079
            if (!$this->performDispatch(new ModelUpdated($this), $usesTransactions)) {
1080
                return false;
1081
            }
1082
        }
1083
1084
        // commit the transaction, if used
1085
        if ($usesTransactions) {
1086
            self::$driver->commitTransaction($this->getConnection());
1087
        }
1088
1089
        return $updated;
1090
    }
1091
1092
    /**
1093
     * Delete the model.
1094
     *
1095
     * @return bool true when the operation was successful
1096
     */
1097
    public function delete(): bool
1098
    {
1099
        if (!$this->hasId) {
1100
            throw new BadMethodCallException('Can only call delete() on an existing model');
1101
        }
1102
1103
        // clear any previous errors
1104
        $this->getErrors()->clear();
1105
1106
        // start a DB transaction if needed
1107
        $usesTransactions = $this->usesTransactions();
1108
        if ($usesTransactions) {
1109
            self::$driver->startTransaction($this->getConnection());
1110
        }
1111
1112
        // dispatch the model.deleting event
1113
        if (!$this->performDispatch(new ModelDeleting($this), $usesTransactions)) {
1114
            return false;
1115
        }
1116
1117
        // perform a hard (default) or soft delete
1118
        $hardDelete = true;
1119
        if (isset($this->deleted_at)) {
1120
            $t = time();
1121
            $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...
1122
            $t = Validator::validateProperty($this, static::definition()->get('deleted_at'), $t);
0 ignored issues
show
Bug introduced by
It seems like static::definition()->get('deleted_at') can be null; however, validateProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1123
            $deleted = self::$driver->updateModel($this, ['deleted_at' => $t]);
1124
            $hardDelete = false;
1125
        } else {
1126
            $deleted = self::$driver->deleteModel($this);
1127
        }
1128
1129
        if ($deleted) {
1130
            // dispatch the model.deleted event
1131
            if (!$this->performDispatch(new ModelDeleted($this), $usesTransactions)) {
1132
                return false;
1133
            }
1134
1135
            if ($hardDelete) {
1136
                $this->_persisted = false;
1137
            }
1138
        }
1139
1140
        // commit the transaction, if used
1141
        if ($usesTransactions) {
1142
            self::$driver->commitTransaction($this->getConnection());
1143
        }
1144
1145
        return $deleted;
1146
    }
1147
1148
    /**
1149
     * Restores a soft-deleted model.
1150
     */
1151
    public function restore(): bool
1152
    {
1153
        if (!isset($this->deleted_at) || !$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...
1154
            throw new BadMethodCallException('Can only call restore() on a soft-deleted model');
1155
        }
1156
1157
        // start a DB transaction if needed
1158
        $usesTransactions = $this->usesTransactions();
1159
        if ($usesTransactions) {
1160
            self::$driver->startTransaction($this->getConnection());
1161
        }
1162
1163
        // dispatch the model.updating event
1164
        if (!$this->performDispatch(new ModelUpdating($this), $usesTransactions)) {
1165
            return false;
1166
        }
1167
1168
        $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...
1169
        $restored = self::$driver->updateModel($this, ['deleted_at' => null]);
1170
1171
        if ($restored) {
1172
            // dispatch the model.updated event
1173
            if (!$this->performDispatch(new ModelUpdated($this), $usesTransactions)) {
1174
                return false;
1175
            }
1176
        }
1177
1178
        // commit the transaction, if used
1179
        if ($usesTransactions) {
1180
            self::$driver->commitTransaction($this->getConnection());
1181
        }
1182
1183
        return $restored;
1184
    }
1185
1186
    /**
1187
     * Checks if the model has been deleted.
1188
     */
1189
    public function isDeleted(): bool
1190
    {
1191
        if (isset($this->deleted_at) && $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...
1192
            return true;
1193
        }
1194
1195
        return !$this->_persisted;
1196
    }
1197
1198
    /////////////////////////////
1199
    // Queries
1200
    /////////////////////////////
1201
1202
    /**
1203
     * Generates a new query instance.
1204
     */
1205
    public static function query(): Query
1206
    {
1207
        // Create a new model instance for the query to ensure
1208
        // that the model's initialize() method gets called.
1209
        // Otherwise, the property definitions will be incomplete.
1210
        $model = new static();
1211
        $query = new Query($model);
1212
1213
        // scope soft-deleted models to only include non-deleted models
1214
        if (self::definition()->has('deleted_at')) {
1215
            $query->where('deleted_at IS NOT NULL');
1216
        }
1217
1218
        return $query;
1219
    }
1220
1221
    /**
1222
     * Generates a new query instance that includes soft-deleted models.
1223
     */
1224
    public static function withDeleted(): Query
1225
    {
1226
        // Create a new model instance for the query to ensure
1227
        // that the model's initialize() method gets called.
1228
        // Otherwise, the property definitions will be incomplete.
1229
        $model = new static();
1230
1231
        return new Query($model);
1232
    }
1233
1234
    /**
1235
     * Finds a single instance of a model given it's ID.
1236
     *
1237
     * @param mixed $id
1238
     *
1239
     * @return static|null
1240
     */
1241
    public static function find($id): ?self
1242
    {
1243
        $ids = [];
1244
        $id = (array) $id;
1245
        foreach (static::$ids as $j => $k) {
1246
            if (isset($id[$j])) {
1247
                $ids[$k] = $id[$j];
1248
            }
1249
        }
1250
1251
        // malformed ID
1252
        if (count($ids) < count(static::$ids)) {
1253
            return null;
1254
        }
1255
1256
        return static::query()->where($ids)->first();
1257
    }
1258
1259
    /**
1260
     * Finds a single instance of a model given it's ID or throws an exception.
1261
     *
1262
     * @param mixed $id
1263
     *
1264
     * @return static
1265
     *
1266
     * @throws ModelNotFoundException when a model could not be found
1267
     */
1268
    public static function findOrFail($id): self
1269
    {
1270
        $model = static::find($id);
1271
        if (!$model) {
1272
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1273
        }
1274
1275
        return $model;
1276
    }
1277
1278
    /**
1279
     * Tells if this model instance has been persisted to the data layer.
1280
     *
1281
     * NOTE: this does not actually perform a check with the data layer
1282
     */
1283
    public function persisted(): bool
1284
    {
1285
        return $this->_persisted;
1286
    }
1287
1288
    /**
1289
     * Loads the model from the storage layer.
1290
     *
1291
     * @return $this
1292
     */
1293
    public function refresh()
1294
    {
1295
        if (!$this->hasId) {
1296
            return $this;
1297
        }
1298
1299
        $values = self::$driver->loadModel($this);
1300
1301
        if (!is_array($values)) {
1302
            return $this;
1303
        }
1304
1305
        // clear any relations
1306
        $this->_relationships = [];
1307
1308
        // type-cast the values that come from the database
1309
        foreach ($values as $k => &$v) {
1310
            if ($property = static::definition()->get($k)) {
1311
                $v = Type::cast($property, $v);
1312
            }
1313
        }
1314
1315
        return $this->refreshWith($values);
1316
    }
1317
1318
    /**
1319
     * Loads values into the model.
1320
     *
1321
     * @param array $values values
1322
     *
1323
     * @return $this
1324
     */
1325
    public function refreshWith(array $values)
1326
    {
1327
        $this->loaded = true;
1328
        $this->_persisted = true;
1329
        $this->_values = $values;
1330
1331
        return $this;
1332
    }
1333
1334
    /**
1335
     * Clears the cache for this model.
1336
     *
1337
     * @return $this
1338
     */
1339
    public function clearCache()
1340
    {
1341
        $this->loaded = false;
1342
        $this->_unsaved = [];
1343
        $this->_values = [];
1344
        $this->_relationships = [];
1345
1346
        return $this;
1347
    }
1348
1349
    /////////////////////////////
1350
    // Relationships
1351
    /////////////////////////////
1352
1353
    /**
1354
     * Gets the relationship manager for a property.
1355
     *
1356
     * @throws InvalidArgumentException when the relationship manager cannot be created
1357
     */
1358
    private function getRelationship(Property $property): AbstractRelation
1359
    {
1360
        $name = $property->getName();
1361
        if (!isset($this->relationships[$name])) {
1362
            $this->relationships[$name] = Relationship::make($this, $property);
1363
        }
1364
1365
        return $this->relationships[$name];
1366
    }
1367
1368
    /**
1369
     * Saves any unsaved models attached through a relationship. This will only
1370
     * save attached models that have not been saved yet.
1371
     */
1372
    private function saveRelationships(bool $usesTransactions): bool
1373
    {
1374
        try {
1375
            foreach ($this->_unsaved as $k => $value) {
1376
                if ($value instanceof self && !$value->persisted()) {
1377
                    $property = static::definition()->get($k);
1378
                    if ($property && !$property->isPersisted()) {
1379
                        $value->saveOrFail();
1380
                        // set the model again to update any ID properties
1381
                        $this->$k = $value;
1382
                    }
1383
                } elseif (is_array($value)) {
1384
                    foreach ($value as $subValue) {
1385
                        if ($subValue instanceof self && !$subValue->persisted()) {
1386
                            $property = static::definition()->get($k);
1387
                            if ($property && !$property->isPersisted()) {
1388
                                $subValue->saveOrFail();
1389
                            }
1390
                        }
1391
                    }
1392
                }
1393
            }
1394
        } catch (ModelException $e) {
1395
            $this->getErrors()->add($e->getMessage());
1396
1397
            if ($usesTransactions) {
1398
                self::$driver->rollBackTransaction($this->getConnection());
1399
            }
1400
1401
            return false;
1402
        }
1403
1404
        return true;
1405
    }
1406
1407
    /**
1408
     * This hydrates an individual property in the model. It can be a
1409
     * scalar value or relationship.
1410
     *
1411
     * @internal
1412
     *
1413
     * @param $value
1414
     */
1415
    public function hydrateValue(string $name, $value): void
1416
    {
1417
        // type-cast the value because it came from the database
1418
        if ($property = static::definition()->get($name)) {
1419
            $this->_values[$name] = Type::cast($property, $value);
1420
        } else {
1421
            $this->_values[$name] = $value;
1422
        }
1423
    }
1424
1425
    /**
1426
     * @deprecated
1427
     *
1428
     * Gets the model(s) for a relationship
1429
     *
1430
     * @param string $k property
1431
     *
1432
     * @throws InvalidArgumentException when the relationship manager cannot be created
1433
     *
1434
     * @return Model|array|null
1435
     */
1436
    public function relation(string $k)
1437
    {
1438
        if (!array_key_exists($k, $this->_relationships)) {
1439
            $relation = Relationship::make($this, static::definition()->get($k));
0 ignored issues
show
Bug introduced by
It seems like static::definition()->get($k) can be null; however, make() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1440
            $this->_relationships[$k] = $relation->getResults();
1441
        }
1442
1443
        return $this->_relationships[$k];
1444
    }
1445
1446
    /**
1447
     * @deprecated
1448
     *
1449
     * Sets the model for a one-to-one relationship (has-one or belongs-to)
1450
     *
1451
     * @return $this
1452
     */
1453
    public function setRelation(string $k, Model $model)
1454
    {
1455
        $this->$k = $model->id();
1456
        $this->_relationships[$k] = $model;
1457
1458
        return $this;
1459
    }
1460
1461
    /**
1462
     * @deprecated
1463
     *
1464
     * Sets the model for a one-to-many relationship
1465
     *
1466
     * @return $this
1467
     */
1468
    public function setRelationCollection(string $k, iterable $models)
1469
    {
1470
        $this->_relationships[$k] = $models;
1471
1472
        return $this;
1473
    }
1474
1475
    /**
1476
     * @deprecated
1477
     *
1478
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null
1479
     *
1480
     * @return $this
1481
     */
1482
    public function clearRelation(string $k)
1483
    {
1484
        $this->$k = null;
1485
        $this->_relationships[$k] = null;
1486
1487
        return $this;
1488
    }
1489
1490
    /////////////////////////////
1491
    // Events
1492
    /////////////////////////////
1493
1494
    /**
1495
     * Gets the event dispatcher.
1496
     */
1497
    public static function getDispatcher($ignoreCache = false): EventDispatcher
1498
    {
1499
        $class = static::class;
1500
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1501
            self::$dispatchers[$class] = new EventDispatcher();
1502
        }
1503
1504
        return self::$dispatchers[$class];
1505
    }
1506
1507
    /**
1508
     * Subscribes to a listener to an event.
1509
     *
1510
     * @param string $event    event name
1511
     * @param int    $priority optional priority, higher #s get called first
1512
     */
1513
    public static function listen(string $event, callable $listener, int $priority = 0)
1514
    {
1515
        static::getDispatcher()->addListener($event, $listener, $priority);
1516
    }
1517
1518
    /**
1519
     * Adds a listener to the model.creating and model.updating events.
1520
     */
1521
    public static function saving(callable $listener, int $priority = 0)
1522
    {
1523
        static::listen(ModelCreating::NAME, $listener, $priority);
1524
        static::listen(ModelUpdating::NAME, $listener, $priority);
1525
    }
1526
1527
    /**
1528
     * Adds a listener to the model.created and model.updated events.
1529
     */
1530
    public static function saved(callable $listener, int $priority = 0)
1531
    {
1532
        static::listen(ModelCreated::NAME, $listener, $priority);
1533
        static::listen(ModelUpdated::NAME, $listener, $priority);
1534
    }
1535
1536
    /**
1537
     * Adds a listener to the model.creating, model.updating, and model.deleting events.
1538
     */
1539
    public static function beforePersist(callable $listener, int $priority = 0)
1540
    {
1541
        static::listen(ModelCreating::NAME, $listener, $priority);
1542
        static::listen(ModelUpdating::NAME, $listener, $priority);
1543
        static::listen(ModelDeleting::NAME, $listener, $priority);
1544
    }
1545
1546
    /**
1547
     * Adds a listener to the model.created, model.updated, and model.deleted events.
1548
     */
1549
    public static function afterPersist(callable $listener, int $priority = 0)
1550
    {
1551
        static::listen(ModelCreated::NAME, $listener, $priority);
1552
        static::listen(ModelUpdated::NAME, $listener, $priority);
1553
        static::listen(ModelDeleted::NAME, $listener, $priority);
1554
    }
1555
1556
    /**
1557
     * Adds a listener to the model.creating event.
1558
     */
1559
    public static function creating(callable $listener, int $priority = 0)
1560
    {
1561
        static::listen(ModelCreating::NAME, $listener, $priority);
1562
    }
1563
1564
    /**
1565
     * Adds a listener to the model.created event.
1566
     */
1567
    public static function created(callable $listener, int $priority = 0)
1568
    {
1569
        static::listen(ModelCreated::NAME, $listener, $priority);
1570
    }
1571
1572
    /**
1573
     * Adds a listener to the model.updating event.
1574
     */
1575
    public static function updating(callable $listener, int $priority = 0)
1576
    {
1577
        static::listen(ModelUpdating::NAME, $listener, $priority);
1578
    }
1579
1580
    /**
1581
     * Adds a listener to the model.updated event.
1582
     */
1583
    public static function updated(callable $listener, int $priority = 0)
1584
    {
1585
        static::listen(ModelUpdated::NAME, $listener, $priority);
1586
    }
1587
1588
    /**
1589
     * Adds a listener to the model.deleting event.
1590
     */
1591
    public static function deleting(callable $listener, int $priority = 0)
1592
    {
1593
        static::listen(ModelDeleting::NAME, $listener, $priority);
1594
    }
1595
1596
    /**
1597
     * Adds a listener to the model.deleted event.
1598
     */
1599
    public static function deleted(callable $listener, int $priority = 0)
1600
    {
1601
        static::listen(ModelDeleted::NAME, $listener, $priority);
1602
    }
1603
1604
    /**
1605
     * Dispatches the given event and checks if it was successful.
1606
     *
1607
     * @return bool true if the events were successfully propagated
1608
     */
1609
    private function performDispatch(AbstractEvent $event, bool $usesTransactions): bool
1610
    {
1611
        static::getDispatcher()->dispatch($event, $event::NAME);
1612
1613
        if (!$event->isPropagationStopped()) {
1614
            return true;
1615
        }
1616
1617
        // when listeners fail roll back any database transaction
1618
        if ($usesTransactions) {
1619
            self::$driver->rollBackTransaction($this->getConnection());
1620
        }
1621
1622
        return false;
1623
    }
1624
1625
    /////////////////////////////
1626
    // Validation
1627
    /////////////////////////////
1628
1629
    /**
1630
     * Gets the error stack for this model.
1631
     */
1632
    public function getErrors(): Errors
1633
    {
1634
        if (!$this->errors) {
1635
            $this->errors = new Errors();
1636
        }
1637
1638
        return $this->errors;
1639
    }
1640
1641
    /**
1642
     * Checks if the model in its current state is valid.
1643
     */
1644
    public function valid(): bool
1645
    {
1646
        // clear any previous errors
1647
        $this->getErrors()->clear();
1648
1649
        // run the validator against the unsaved model values
1650
        $validated = true;
1651
        foreach ($this->_unsaved as $k => &$v) {
1652
            $property = static::definition()->get($k);
1653
            $validated = Validator::validateProperty($this, $property, $v) && $validated;
0 ignored issues
show
Bug introduced by
It seems like $property defined by static::definition()->get($k) on line 1652 can be null; however, Pulsar\Validator::validateProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1654
        }
1655
1656
        return $validated;
1657
    }
1658
}
1659