Completed
Push — master ( ab368d...b57266 )
by Jared
05:39
created

Model::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\ModelCreated;
20
use Pulsar\Event\ModelCreating;
21
use Pulsar\Event\ModelDeleted;
22
use Pulsar\Event\ModelDeleting;
23
use Pulsar\Event\ModelUpdated;
24
use Pulsar\Event\ModelUpdating;
25
use Pulsar\Exception\DriverMissingException;
26
use Pulsar\Exception\MassAssignmentException;
27
use Pulsar\Exception\ModelException;
28
use Pulsar\Exception\ModelNotFoundException;
29
use Pulsar\Relation\AbstractRelation;
30
use Pulsar\Relation\Relationship;
31
use Throwable;
32
33
/**
34
 * @mixin Query
35
 */
36
abstract class Model implements ArrayAccess
37
{
38
    const DEFAULT_ID_NAME = 'id';
39
40
    /////////////////////////////
41
    // Model visible variables
42
    /////////////////////////////
43
44
    /**
45
     * List of model ID property names.
46
     *
47
     * @var array
48
     */
49
    protected static $ids = [self::DEFAULT_ID_NAME];
50
51
    /**
52
     * Property definitions expressed as a key-value map with
53
     * property names as the keys.
54
     * i.e. ['enabled' => ['type' => Type::BOOLEAN]].
55
     *
56
     * @var array
57
     */
58
    protected static $properties = [];
59
60
    /**
61
     * @var array
62
     */
63
    protected $_values = [];
64
65
    /**
66
     * @var array
67
     */
68
    private $_unsaved = [];
69
70
    /**
71
     * @var bool
72
     */
73
    protected $_persisted = false;
74
75
    /**
76
     * @var array
77
     */
78
    protected $_relationships = [];
79
80
    /**
81
     * @var AbstractRelation[]
82
     */
83
    private $relationships = [];
84
85
    /////////////////////////////
86
    // Base model variables
87
    /////////////////////////////
88
89
    /**
90
     * @var array
91
     */
92
    private static $initialized = [];
93
94
    /**
95
     * @var DriverInterface
96
     */
97
    private static $driver;
98
99
    /**
100
     * @var array
101
     */
102
    private static $accessors = [];
103
104
    /**
105
     * @var array
106
     */
107
    private static $mutators = [];
108
109
    /**
110
     * @var string
111
     */
112
    private $tablename;
113
114
    /**
115
     * @var bool
116
     */
117
    private $hasId;
118
119
    /**
120
     * @var array
121
     */
122
    private $idValues;
123
124
    /**
125
     * @var bool
126
     */
127
    private $loaded = false;
128
129
    /**
130
     * @var Errors
131
     */
132
    private $errors;
133
134
    /**
135
     * @var bool
136
     */
137
    private $ignoreUnsaved = false;
138
139
    /**
140
     * Creates a new model object.
141
     *
142
     * @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...
143
     * @param array                    $values optional key-value map to pre-seed model
144
     */
145
    public function __construct(array $values = [])
146
    {
147
        // initialize the model
148
        $this->init();
149
150
        $ids = [];
151
        $this->hasId = true;
152
        foreach (static::$ids as $name) {
153
            $id = null;
154
            if (array_key_exists($name, $values)) {
155
                $idProperty = static::definition()->get($name);
156
                $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 155 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...
157
            }
158
159
            $ids[$name] = $id;
160
            $this->hasId = $this->hasId && $id;
161
        }
162
163
        $this->idValues = $ids;
164
165
        // load any given values
166
        if ($this->hasId && count($values) > count($ids)) {
167
            $this->refreshWith($values);
168
        } elseif (!$this->hasId) {
169
            $this->_unsaved = $values;
170
        } else {
171
            $this->_values = $this->idValues;
172
        }
173
    }
174
175
    /**
176
     * Performs initialization on this model.
177
     */
178
    private function init()
179
    {
180
        // ensure the initialize function is called only once
181
        $k = static::class;
182
        if (!isset(self::$initialized[$k])) {
183
            $this->initialize();
184
            self::$initialized[$k] = true;
185
        }
186
    }
187
188
    /**
189
     * The initialize() method is called once per model. This is a great
190
     * place to install event listeners. Any methods on the model that have
191
     * "autoInitialize" in the name will automatically be called.
192
     */
193
    protected function initialize()
194
    {
195
        // Use reflection to automatically call any method here that has a name
196
        // that starts with "autoInitialize". This is useful for traits to install listeners.
197
        $methods = get_class_methods(static::class);
198
        foreach ($methods as $method) {
199
            if (0 === strpos($method, 'autoInitialize')) {
200
                $this->$method();
201
            }
202
        }
203
    }
204
205
    /**
206
     * Sets the driver for all models.
207
     */
208
    public static function setDriver(DriverInterface $driver)
209
    {
210
        self::$driver = $driver;
211
    }
212
213
    /**
214
     * Gets the driver for all models.
215
     *
216
     * @throws DriverMissingException when a driver has not been set yet
217
     */
218
    public static function getDriver(): DriverInterface
219
    {
220
        if (!self::$driver) {
221
            throw new DriverMissingException('A model driver has not been set yet.');
222
        }
223
224
        return self::$driver;
225
    }
226
227
    /**
228
     * Clears the driver for all models.
229
     */
230
    public static function clearDriver()
231
    {
232
        self::$driver = null;
233
    }
234
235
    /**
236
     * Gets the name of the model, i.e. User.
237
     */
238
    public static function modelName(): string
239
    {
240
        // strip namespacing
241
        $paths = explode('\\', static::class);
242
243
        return end($paths);
244
    }
245
246
    /**
247
     * Gets the model ID.
248
     *
249
     * @return string|number|false ID
250
     */
251
    public function id()
252
    {
253
        if (!$this->hasId) {
254
            return false;
255
        }
256
257
        if (1 == count($this->idValues)) {
258
            return reset($this->idValues);
259
        }
260
261
        $result = [];
262
        foreach (static::$ids as $k) {
263
            $result[] = $this->idValues[$k];
264
        }
265
266
        return implode(',', $result);
267
    }
268
269
    /**
270
     * Gets a key-value map of the model ID.
271
     *
272
     * @return array ID map
273
     */
274
    public function ids(): array
275
    {
276
        return $this->idValues;
277
    }
278
279
    /**
280
     * Checks if the model has an identifier present.
281
     * This does not indicate whether the model has been
282
     * persisted to the database or loaded from the database.
283
     */
284
    public function hasId(): bool
285
    {
286
        return $this->hasId;
287
    }
288
289
    /////////////////////////////
290
    // Magic Methods
291
    /////////////////////////////
292
293
    /**
294
     * Converts the model into a string.
295
     *
296
     * @return string
297
     */
298
    public function __toString()
299
    {
300
        $values = array_merge($this->_values, $this->_unsaved, $this->idValues);
301
        ksort($values);
302
303
        return static::class.'('.json_encode($values, JSON_PRETTY_PRINT).')';
304
    }
305
306
    /**
307
     * Shortcut to a get() call for a given property.
308
     *
309
     * @param string $name
310
     *
311
     * @return mixed
312
     */
313
    public function __get($name)
314
    {
315
        $result = $this->get([$name]);
316
317
        return reset($result);
318
    }
319
320
    /**
321
     * Sets an unsaved value.
322
     *
323
     * @param string $name
324
     * @param mixed  $value
325
     */
326
    public function __set($name, $value)
327
    {
328
        // if changing property, remove relation model
329
        if (isset($this->_relationships[$name])) {
330
            unset($this->_relationships[$name]);
331
        }
332
333
        // call any mutators
334
        $mutator = self::getMutator($name);
335
        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...
336
            $this->_unsaved[$name] = $this->$mutator($value);
337
        } else {
338
            $this->_unsaved[$name] = $value;
339
        }
340
341
        // set local ID property on belongs_to relationship
342
        if (static::definition()->has($name)) {
343
            $property = static::definition()->get($name);
344
            if (Relationship::BELONGS_TO == $property->getRelationshipType() && !$property->isPersisted()) {
345
                if ($value instanceof self) {
346
                    $this->_unsaved[$property->getLocalKey()] = $value->{$property->getForeignKey()};
347
                } elseif (null === $value) {
348
                    $this->_unsaved[$property->getLocalKey()] = null;
349
                } else {
350
                    throw new ModelException('The value set on the "'.$name.'" property must be a model or null.');
351
                }
352
            }
353
        }
354
    }
355
356
    /**
357
     * Checks if an unsaved value or property exists by this name.
358
     *
359
     * @param string $name
360
     *
361
     * @return bool
362
     */
363
    public function __isset($name)
364
    {
365
        // isset() must return true for any value that could be returned by offsetGet
366
        // because many callers will first check isset() to see if the value is accessible.
367
        // This method is not supposed to only be valid for unsaved values, or properties
368
        // that have a value.
369
        return array_key_exists($name, $this->_unsaved) || static::definition()->has($name);
370
    }
371
372
    /**
373
     * Unsets an unsaved value.
374
     *
375
     * @param string $name
376
     */
377
    public function __unset($name)
378
    {
379
        if (array_key_exists($name, $this->_unsaved)) {
380
            // if changing property, remove relation model
381
            if (isset($this->_relationships[$name])) {
382
                unset($this->_relationships[$name]);
383
            }
384
385
            unset($this->_unsaved[$name]);
386
        }
387
    }
388
389
    /////////////////////////////
390
    // ArrayAccess Interface
391
    /////////////////////////////
392
393
    public function offsetExists($offset)
394
    {
395
        return isset($this->$offset);
396
    }
397
398
    public function offsetGet($offset)
399
    {
400
        return $this->$offset;
401
    }
402
403
    public function offsetSet($offset, $value)
404
    {
405
        $this->$offset = $value;
406
    }
407
408
    public function offsetUnset($offset)
409
    {
410
        unset($this->$offset);
411
    }
412
413
    public static function __callStatic($name, $parameters)
414
    {
415
        // Any calls to unkown static methods should be deferred to
416
        // the query. This allows calls like User::where()
417
        // to replace User::query()->where().
418
        return call_user_func_array([static::query(), $name], $parameters);
419
    }
420
421
    /////////////////////////////
422
    // Property Definitions
423
    /////////////////////////////
424
425
    /**
426
     * Gets the model definition.
427
     */
428
    public static function definition(): Definition
429
    {
430
        return DefinitionBuilder::get(static::class);
431
    }
432
433
    /**
434
     * The buildDefinition() method is called once per model. It's used
435
     * to generate the model definition. This is a great place to add any
436
     * dynamic model properties.
437
     */
438
    public static function buildDefinition(): Definition
439
    {
440
        // Use reflection to automatically call any method on the model that has a name
441
        // that starts with "buildDefinition". This is useful for traits to add properties.
442
        $methods = get_class_methods(static::class);
443
        foreach ($methods as $method) {
444
            if (0 === strpos($method, 'autoDefinition')) {
445
                static::$method();
446
            }
447
        }
448
449
        return DefinitionBuilder::build(static::$properties, static::class);
450
    }
451
452
    /**
453
     * Gets the names of the model ID properties.
454
     */
455
    public static function getIDProperties(): array
456
    {
457
        return static::$ids;
458
    }
459
460
    /**
461
     * Gets the mutator method name for a given property name.
462
     * Looks for methods in the form of `setPropertyValue`.
463
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
464
     *
465
     * @param string $property property
466
     *
467
     * @return string|null method name if it exists
468
     */
469
    public static function getMutator(string $property): ?string
470
    {
471
        $class = static::class;
472
473
        $k = $class.':'.$property;
474
        if (!array_key_exists($k, self::$mutators)) {
475
            $inflector = Inflector::get();
476
            $method = 'set'.$inflector->camelize($property).'Value';
477
478
            if (!method_exists($class, $method)) {
479
                $method = null;
480
            }
481
482
            self::$mutators[$k] = $method;
483
        }
484
485
        return self::$mutators[$k];
486
    }
487
488
    /**
489
     * Gets the accessor method name for a given property name.
490
     * Looks for methods in the form of `getPropertyValue`.
491
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
492
     *
493
     * @param string $property property
494
     *
495
     * @return string|null method name if it exists
496
     */
497
    public static function getAccessor(string $property): ?string
498
    {
499
        $class = static::class;
500
501
        $k = $class.':'.$property;
502
        if (!array_key_exists($k, self::$accessors)) {
503
            $inflector = Inflector::get();
504
            $method = 'get'.$inflector->camelize($property).'Value';
505
506
            if (!method_exists($class, $method)) {
507
                $method = null;
508
            }
509
510
            self::$accessors[$k] = $method;
511
        }
512
513
        return self::$accessors[$k];
514
    }
515
516
    /////////////////////////////
517
    // CRUD Operations
518
    /////////////////////////////
519
520
    /**
521
     * Gets the table name for storing this model.
522
     */
523
    public function getTablename(): string
524
    {
525
        if (!$this->tablename) {
526
            $inflector = Inflector::get();
527
528
            $this->tablename = $inflector->camelize($inflector->pluralize(static::modelName()));
529
        }
530
531
        return $this->tablename;
532
    }
533
534
    /**
535
     * Gets the ID of the connection in the connection manager
536
     * that stores this model.
537
     */
538
    public function getConnection(): ?string
539
    {
540
        return null;
541
    }
542
543
    protected function usesTransactions(): bool
544
    {
545
        return false;
546
    }
547
548
    /**
549
     * Saves the model.
550
     *
551
     * @return bool true when the operation was successful
552
     */
553
    public function save(): bool
554
    {
555
        if (!$this->hasId) {
556
            return $this->create();
557
        }
558
559
        return $this->set();
560
    }
561
562
    /**
563
     * Saves the model. Throws an exception when the operation fails.
564
     *
565
     * @throws ModelException when the model cannot be saved
566
     */
567
    public function saveOrFail()
568
    {
569
        if (!$this->save()) {
570
            $msg = 'Failed to save '.static::modelName();
571
            if ($validationErrors = $this->getErrors()->all()) {
572
                $msg .= ': '.implode(', ', $validationErrors);
573
            }
574
575
            throw new ModelException($msg);
576
        }
577
    }
578
579
    /**
580
     * Creates a new model.
581
     *
582
     * @param array $data optional key-value properties to set
583
     *
584
     * @return bool true when the operation was successful
585
     *
586
     * @throws BadMethodCallException when called on an existing model
587
     */
588
    public function create(array $data = []): bool
589
    {
590
        if ($this->hasId) {
591
            throw new BadMethodCallException('Cannot call create() on an existing model');
592
        }
593
594
        // mass assign values passed into create()
595
        $this->setValues($data);
596
597
        // clear any previous errors
598
        $this->getErrors()->clear();
599
600
        // start a DB transaction if needed
601
        $usesTransactions = $this->usesTransactions();
602
        if ($usesTransactions) {
603
            self::$driver->startTransaction($this->getConnection());
604
        }
605
606
        try {
607
            // dispatch the model.creating event
608
            if (!EventManager::dispatch($this, new ModelCreating($this), $usesTransactions)) {
609
                return false;
610
            }
611
612
            $requiredProperties = [];
613
            foreach (static::definition()->all() as $name => $property) {
614
                // build a list of the required properties
615
                if ($property->isRequired()) {
616
                    $requiredProperties[] = $property;
617
                }
618
619
                // add in default values
620
                if (!array_key_exists($name, $this->_unsaved) && $property->hasDefault()) {
621
                    $this->_unsaved[$name] = $property->getDefault();
622
                }
623
            }
624
625
            // save any relationships
626
            if (!$this->saveRelationships($usesTransactions)) {
627
                return false;
628
            }
629
630
            // validate the values being saved
631
            $validated = true;
632
            $insertArray = [];
633
            $preservedValues = [];
634
            foreach ($this->_unsaved as $name => $value) {
635
                // exclude if value does not map to a property
636
                $property = static::definition()->get($name);
637
                if (!$property) {
638
                    continue;
639
                }
640
641
                // check if this property is persisted to the DB
642
                if (!$property->isPersisted()) {
643
                    $preservedValues[$name] = $value;
644
                    continue;
645
                }
646
647
                // cannot insert immutable values
648
                // (unless using the default value)
649
                if ($property->isImmutable() && $value !== $property->getDefault()) {
650
                    continue;
651
                }
652
653
                $validated = $validated && Validator::validateProperty($this, $property, $value);
654
                $insertArray[$name] = $value;
655
            }
656
657
            // check for required fields
658
            foreach ($requiredProperties as $property) {
659
                $name = $property->getName();
660
                if (!isset($insertArray[$name]) && !isset($preservedValues[$name])) {
661
                    $context = [
662
                        'field' => $name,
663
                        'field_name' => $property->getTitle($this),
664
                    ];
665
                    $this->getErrors()->add('pulsar.validation.required', $context);
666
667
                    $validated = false;
668
                }
669
            }
670
671
            if (!$validated) {
672
                // when validations fail roll back any database transaction
673
                if ($usesTransactions) {
674
                    self::$driver->rollBackTransaction($this->getConnection());
675
                }
676
677
                return false;
678
            }
679
680
            $created = self::$driver->createModel($this, $insertArray);
681
682
            if ($created) {
683
                // determine the model's new ID
684
                $this->getNewId();
685
686
                // store the persisted values to the in-memory cache
687
                $this->_unsaved = [];
688
                $hydrateValues = array_replace($this->idValues, $preservedValues);
689
690
                // only type-cast the values that were converted to the database format
691
                foreach ($insertArray as $k => $v) {
692
                    if ($property = static::definition()->get($k)) {
693
                        $hydrateValues[$k] = Type::cast($property, $v);
694
                    } else {
695
                        $hydrateValues[$k] = $v;
696
                    }
697
                }
698
                $this->refreshWith($hydrateValues);
699
700
                // dispatch the model.created event
701
                if (!EventManager::dispatch($this, new ModelCreated($this), $usesTransactions)) {
702
                    return false;
703
                }
704
            }
705
        } catch (Throwable $e) {
706
            // roll back the transaction, if used
707
            if ($usesTransactions) {
708
                self::$driver->rollBackTransaction($this->getConnection());
709
            }
710
711
            // now that the transaction is rolled back we can rethrow
712
            throw $e;
713
        }
714
715
        // commit the transaction, if used
716
        if ($usesTransactions) {
717
            self::$driver->commitTransaction($this->getConnection());
718
        }
719
720
        return $created;
721
    }
722
723
    /**
724
     * Ignores unsaved values when fetching the next value.
725
     *
726
     * @return $this
727
     */
728
    public function ignoreUnsaved()
729
    {
730
        $this->ignoreUnsaved = true;
731
732
        return $this;
733
    }
734
735
    /**
736
     * Fetches property values from the model.
737
     *
738
     * This method looks up values in this order:
739
     * IDs, local cache, unsaved values, storage layer, defaults
740
     *
741
     * @param array $properties list of property names to fetch values of
742
     */
743
    public function get(array $properties): array
744
    {
745
        // check if unsaved values will be returned
746
        $ignoreUnsaved = $this->ignoreUnsaved;
747
        $this->ignoreUnsaved = false;
748
749
        // Check if the model needs to be loaded from the database. This
750
        // is used if an ID was supplied for the model but the values have
751
        // not been hydrated from the database. We only want to load values
752
        // from the database if there are properties requested that are both
753
        // persisted to the database AND do not already have a value present.
754
        $this->loadIfNeeded($properties, $ignoreUnsaved);
755
756
        // build a key-value map of the requested properties
757
        $return = [];
758
        foreach ($properties as $k) {
759
            $return[$k] = $this->getValue($k, $ignoreUnsaved);
760
        }
761
762
        return $return;
763
    }
764
765
    /**
766
     * Loads the model from the database if needed.
767
     */
768
    private function loadIfNeeded(array $properties, bool $ignoreUnsaved): void
769
    {
770
        if ($this->loaded | !$this->hasId) {
771
            return;
772
        }
773
774
        foreach ($properties as $k) {
775
            if (!isset($this->_values[$k]) && ($ignoreUnsaved || !isset($this->_unsaved[$k]))) {
776
                $property = static::definition()->get($k);
777
                if ($property && $property->isPersisted()) {
778
                    $this->refresh();
779
780
                    return;
781
                }
782
            }
783
        }
784
    }
785
786
    /**
787
     * Gets a property value from the model.
788
     *
789
     * Values are looked up in this order:
790
     *  1. unsaved values
791
     *  2. local values
792
     *  3. default value
793
     *  4. null
794
     *
795
     * @return mixed
796
     */
797
    private function getValue(string $name, bool $ignoreUnsaved)
798
    {
799
        $value = null;
800
        if (!$ignoreUnsaved && array_key_exists($name, $this->_unsaved)) {
801
            $value = $this->_unsaved[$name];
802
        } elseif (array_key_exists($name, $this->_values)) {
803
            $value = $this->_values[$name];
804
        } elseif ($property = static::definition()->get($name)) {
805
            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...
806
                $relationship = $this->getRelationship($property);
807
                $value = $this->_values[$name] = $relationship->getResults();
808
            } else {
809
                $value = $this->_values[$name] = $property->getDefault();
810
            }
811
        }
812
813
        // call any accessors
814
        if ($accessor = self::getAccessor($name)) {
815
            $value = $this->$accessor($value);
816
        }
817
818
        return $value;
819
    }
820
821
    /**
822
     * Populates a newly created model with its ID.
823
     */
824
    private function getNewId()
825
    {
826
        $ids = [];
827
        $namedIds = [];
828
        foreach (static::$ids as $k) {
829
            // attempt use the supplied value if the ID property is mutable
830
            $property = static::definition()->get($k);
831
            if (!$property->isImmutable() && isset($this->_unsaved[$k])) {
832
                $id = $this->_unsaved[$k];
833
            } else {
834
                // type-cast the value because it came from the database
835
                $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 830 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...
836
            }
837
838
            $ids[] = $id;
839
            $namedIds[$k] = $id;
840
        }
841
842
        $this->hasId = true;
843
        $this->idValues = $namedIds;
844
        $this->_values = array_replace($this->_values, $namedIds);
845
    }
846
847
    protected function getMassAssignmentWhitelist(): ?array
848
    {
849
        // Deprecated: this is deprecated
850
        if (property_exists($this, 'permitted')) {
851
            return static::$permitted;
852
        }
853
854
        return null;
855
    }
856
857
    protected function getMassAssignmentBlacklist(): ?array
858
    {
859
        // Deprecated: this is deprecated
860
        if (property_exists($this, 'protected')) {
861
            return static::$protected;
862
        }
863
864
        return null;
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 return
914
        $properties = [];
915
        foreach (static::definition()->all() as $property) {
916
            if ($property->isInArray()) {
917
                $properties[] = $property->getName();
918
            }
919
        }
920
921
        // Deprecated: this setting is deprecated
922
        // add any appended properties
923
        if (property_exists(static::class, 'appended')) {
924
            $properties = array_merge($properties, static::$appended);
925
        }
926
927
        // get the values for the properties
928
        $result = $this->get($properties);
929
930
        foreach ($result as $k => &$value) {
931
            // convert arrays of models to arrays
932
            if (is_array($value)) {
933
                foreach ($value as &$subValue) {
934
                    if ($subValue instanceof Model) {
935
                        $subValue = $subValue->toArray();
936
                    }
937
                }
938
            }
939
940
            // convert any models to arrays
941
            if ($value instanceof self) {
942
                $value = $value->toArray();
943
            }
944
        }
945
946
        return $result;
947
    }
948
949
    /**
950
     * Checks if the unsaved value for a property is present and
951
     * is different from the original value.
952
     *
953
     * @property string|null $name
954
     * @property bool        $hasChanged when true, checks if the unsaved value is different from the saved value
955
     */
956
    public function dirty(?string $name = null, bool $hasChanged = false): bool
957
    {
958
        if (!$name) {
959
            if ($hasChanged) {
960
                throw new \RuntimeException('Checking if all properties have changed is not supported');
961
            }
962
963
            return count($this->_unsaved) > 0;
964
        }
965
966
        if (!array_key_exists($name, $this->_unsaved)) {
967
            return false;
968
        }
969
970
        if (!$hasChanged) {
971
            return true;
972
        }
973
974
        return $this->$name !== $this->ignoreUnsaved()->$name;
975
    }
976
977
    /**
978
     * Updates the model.
979
     *
980
     * @param array $data optional key-value properties to set
981
     *
982
     * @return bool true when the operation was successful
983
     *
984
     * @throws BadMethodCallException when not called on an existing model
985
     */
986
    public function set(array $data = []): bool
987
    {
988
        if (!$this->hasId) {
989
            throw new BadMethodCallException('Can only call set() on an existing model');
990
        }
991
992
        // mass assign values passed into set()
993
        $this->setValues($data);
994
995
        // clear any previous errors
996
        $this->getErrors()->clear();
997
998
        // not updating anything?
999
        if (0 == count($this->_unsaved)) {
1000
            return true;
1001
        }
1002
1003
        // start a DB transaction if needed
1004
        $usesTransactions = $this->usesTransactions();
1005
        if ($usesTransactions) {
1006
            self::$driver->startTransaction($this->getConnection());
1007
        }
1008
1009
        try {
1010
            // dispatch the model.updating event
1011
            if (!EventManager::dispatch($this, new ModelUpdating($this), $usesTransactions)) {
1012
                return false;
1013
            }
1014
1015
            // save any relationships
1016
            if (!$this->saveRelationships($usesTransactions)) {
1017
                return false;
1018
            }
1019
1020
            // validate the values being saved
1021
            $validated = true;
1022
            $updateArray = [];
1023
            $preservedValues = [];
1024
            foreach ($this->_unsaved as $name => $value) {
1025
                // exclude if value does not map to a property
1026
                if (!static::definition()->has($name)) {
1027
                    continue;
1028
                }
1029
1030
                $property = static::definition()->get($name);
1031
1032
                // check if this property is persisted to the DB
1033
                if (!$property->isPersisted()) {
1034
                    $preservedValues[$name] = $value;
1035
                    continue;
1036
                }
1037
1038
                // can only modify mutable properties
1039
                if (!$property->isMutable()) {
1040
                    continue;
1041
                }
1042
1043
                $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 1030 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...
1044
                $updateArray[$name] = $value;
1045
            }
1046
1047
            if (!$validated) {
1048
                // when validations fail roll back any database transaction
1049
                if ($usesTransactions) {
1050
                    self::$driver->rollBackTransaction($this->getConnection());
1051
                }
1052
1053
                return false;
1054
            }
1055
1056
            $updated = self::$driver->updateModel($this, $updateArray);
1057
1058
            if ($updated) {
1059
                // store the persisted values to the in-memory cache
1060
                $this->_unsaved = [];
1061
                $hydrateValues = array_replace($this->_values, $preservedValues);
1062
1063
                // only type-cast the values that were converted to the database format
1064
                foreach ($updateArray as $k => $v) {
1065
                    if ($property = static::definition()->get($k)) {
1066
                        $hydrateValues[$k] = Type::cast($property, $v);
1067
                    } else {
1068
                        $hydrateValues[$k] = $v;
1069
                    }
1070
                }
1071
                $this->refreshWith($hydrateValues);
1072
1073
                // dispatch the model.updated event
1074
                if (!EventManager::dispatch($this, new ModelUpdated($this), $usesTransactions)) {
1075
                    return false;
1076
                }
1077
            }
1078
        } catch (Throwable $e) {
1079
            // roll back the transaction, if used
1080
            if ($usesTransactions) {
1081
                self::$driver->rollBackTransaction($this->getConnection());
1082
            }
1083
1084
            // now that the transaction is rolled back we can rethrow
1085
            throw $e;
1086
        }
1087
1088
        // commit the transaction, if used
1089
        if ($usesTransactions) {
1090
            self::$driver->commitTransaction($this->getConnection());
1091
        }
1092
1093
        return $updated;
1094
    }
1095
1096
    /**
1097
     * Delete the model.
1098
     *
1099
     * @return bool true when the operation was successful
1100
     */
1101
    public function delete(): bool
1102
    {
1103
        if (!$this->hasId) {
1104
            throw new BadMethodCallException('Can only call delete() on an existing model');
1105
        }
1106
1107
        // clear any previous errors
1108
        $this->getErrors()->clear();
1109
1110
        // start a DB transaction if needed
1111
        $usesTransactions = $this->usesTransactions();
1112
        if ($usesTransactions) {
1113
            self::$driver->startTransaction($this->getConnection());
1114
        }
1115
1116
        try {
1117
            // dispatch the model.deleting event
1118
            if (!EventManager::dispatch($this, new ModelDeleting($this), $usesTransactions)) {
1119
                return false;
1120
            }
1121
1122
            // perform the delete operation in the data store
1123
            $deleted = $this->performDelete();
1124
1125
            if ($deleted) {
1126
                // dispatch the model.deleted event
1127
                if (!EventManager::dispatch($this, new ModelDeleted($this), $usesTransactions)) {
1128
                    $this->_persisted = true;
1129
1130
                    return false;
1131
                }
1132
            }
1133
        } catch (Throwable $e) {
1134
            // roll back the transaction, if used
1135
            if ($usesTransactions) {
1136
                self::$driver->rollBackTransaction($this->getConnection());
1137
            }
1138
1139
            // now that the transaction is rolled back we can rethrow
1140
            throw $e;
1141
        }
1142
1143
        // commit the transaction, if used
1144
        if ($usesTransactions) {
1145
            self::$driver->commitTransaction($this->getConnection());
1146
        }
1147
1148
        return $deleted;
1149
    }
1150
1151
    /**
1152
     * Performs the delete operation against the database driver.
1153
     * This is a separate protected method to allow traits to override
1154
     * the behavior (i.e. soft deletes).
1155
     */
1156
    protected function performDelete(): bool
1157
    {
1158
        $deleted = self::$driver->deleteModel($this);
1159
        if ($deleted) {
1160
            $this->_persisted = false;
1161
        }
1162
1163
        return $deleted;
1164
    }
1165
1166
    /**
1167
     * Checks if the model has been deleted.
1168
     */
1169
    public function isDeleted(): bool
1170
    {
1171
        return !$this->_persisted;
1172
    }
1173
1174
    /////////////////////////////
1175
    // Queries
1176
    /////////////////////////////
1177
1178
    /**
1179
     * Generates a new query instance.
1180
     */
1181
    public static function query(): Query
1182
    {
1183
        // Create a new model instance for the query to ensure
1184
        // that the model's initialize() method gets called.
1185
        // Otherwise, the property definitions will be incomplete.
1186
        return new Query(new static());
1187
    }
1188
1189
    /**
1190
     * Finds a single instance of a model given it's ID.
1191
     *
1192
     * @param mixed $id
1193
     *
1194
     * @return static|null
1195
     */
1196
    public static function find($id)
1197
    {
1198
        $ids = [];
1199
        $id = (array) $id;
1200
        foreach (static::$ids as $j => $k) {
1201
            if (isset($id[$j])) {
1202
                $ids[$k] = $id[$j];
1203
            }
1204
        }
1205
1206
        // malformed ID
1207
        if (count($ids) < count(static::$ids)) {
1208
            return null;
1209
        }
1210
1211
        return static::query()->where($ids)->oneOrNull();
1212
    }
1213
1214
    /**
1215
     * Finds a single instance of a model given it's ID or throws an exception.
1216
     *
1217
     * @param mixed $id
1218
     *
1219
     * @return static
1220
     *
1221
     * @throws ModelNotFoundException when a model could not be found
1222
     */
1223
    public static function findOrFail($id)
1224
    {
1225
        $model = static::find($id);
1226
        if (!$model) {
1227
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1228
        }
1229
1230
        return $model;
1231
    }
1232
1233
    /**
1234
     * Tells if this model instance has been persisted to the data layer.
1235
     *
1236
     * NOTE: this does not actually perform a check with the data layer
1237
     */
1238
    public function persisted(): bool
1239
    {
1240
        return $this->_persisted;
1241
    }
1242
1243
    /**
1244
     * Loads the model from the storage layer.
1245
     *
1246
     * @return $this
1247
     */
1248
    public function refresh()
1249
    {
1250
        if (!$this->hasId) {
1251
            return $this;
1252
        }
1253
1254
        $values = self::$driver->loadModel($this);
1255
1256
        if (!is_array($values)) {
1257
            return $this;
1258
        }
1259
1260
        // clear any relations
1261
        $this->_relationships = [];
1262
1263
        // type-cast the values that come from the database
1264
        foreach ($values as $k => &$v) {
1265
            if ($property = static::definition()->get($k)) {
1266
                $v = Type::cast($property, $v);
1267
            }
1268
        }
1269
1270
        return $this->refreshWith($values);
1271
    }
1272
1273
    /**
1274
     * Loads values into the model.
1275
     *
1276
     * @param array $values values
1277
     *
1278
     * @return $this
1279
     */
1280
    public function refreshWith(array $values)
1281
    {
1282
        $this->loaded = true;
1283
        $this->_persisted = true;
1284
        $this->_values = $values;
1285
1286
        return $this;
1287
    }
1288
1289
    /**
1290
     * Clears the cache for this model.
1291
     *
1292
     * @return $this
1293
     */
1294
    public function clearCache()
1295
    {
1296
        $this->loaded = false;
1297
        $this->_unsaved = [];
1298
        $this->_values = [];
1299
        $this->_relationships = [];
1300
1301
        return $this;
1302
    }
1303
1304
    /////////////////////////////
1305
    // Relationships
1306
    /////////////////////////////
1307
1308
    /**
1309
     * Gets the relationship manager for a property.
1310
     *
1311
     * @throws InvalidArgumentException when the relationship manager cannot be created
1312
     */
1313
    private function getRelationship(Property $property): AbstractRelation
1314
    {
1315
        $name = $property->getName();
1316
        if (!isset($this->relationships[$name])) {
1317
            $this->relationships[$name] = Relationship::make($this, $property);
1318
        }
1319
1320
        return $this->relationships[$name];
1321
    }
1322
1323
    /**
1324
     * Saves any unsaved models attached through a relationship. This will only
1325
     * save attached models that have not been saved yet.
1326
     */
1327
    private function saveRelationships(bool $usesTransactions): bool
1328
    {
1329
        try {
1330
            foreach ($this->_unsaved as $k => $value) {
1331
                if ($value instanceof self && !$value->persisted()) {
1332
                    $property = static::definition()->get($k);
1333
                    if ($property && !$property->isPersisted()) {
1334
                        $value->saveOrFail();
1335
                        // set the model again to update any ID properties
1336
                        $this->$k = $value;
1337
                    }
1338
                } elseif (is_array($value)) {
1339
                    foreach ($value as $subValue) {
1340
                        if ($subValue instanceof self && !$subValue->persisted()) {
1341
                            $property = static::definition()->get($k);
1342
                            if ($property && !$property->isPersisted()) {
1343
                                $subValue->saveOrFail();
1344
                            }
1345
                        }
1346
                    }
1347
                }
1348
            }
1349
        } catch (ModelException $e) {
1350
            $this->getErrors()->add($e->getMessage());
1351
1352
            if ($usesTransactions) {
1353
                self::$driver->rollBackTransaction($this->getConnection());
1354
            }
1355
1356
            return false;
1357
        }
1358
1359
        return true;
1360
    }
1361
1362
    /**
1363
     * This hydrates an individual property in the model. It can be a
1364
     * scalar value or relationship.
1365
     *
1366
     * @internal
1367
     *
1368
     * @param $value
1369
     */
1370
    public function hydrateValue(string $name, $value): void
1371
    {
1372
        // type-cast the value because it came from the database
1373
        if ($property = static::definition()->get($name)) {
1374
            $this->_values[$name] = Type::cast($property, $value);
1375
        } else {
1376
            $this->_values[$name] = $value;
1377
        }
1378
    }
1379
1380
    /**
1381
     * @deprecated
1382
     *
1383
     * Gets the model(s) for a relationship
1384
     *
1385
     * @param string $k property
1386
     *
1387
     * @throws InvalidArgumentException when the relationship manager cannot be created
1388
     *
1389
     * @return Model|array|null
1390
     */
1391
    public function relation(string $k)
1392
    {
1393
        if (!array_key_exists($k, $this->_relationships)) {
1394
            $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...
1395
            $this->_relationships[$k] = $relation->getResults();
1396
        }
1397
1398
        return $this->_relationships[$k];
1399
    }
1400
1401
    /**
1402
     * @deprecated
1403
     *
1404
     * Sets the model for a one-to-one relationship (has-one or belongs-to)
1405
     *
1406
     * @return $this
1407
     */
1408
    public function setRelation(string $k, Model $model)
1409
    {
1410
        $this->$k = $model->id();
1411
        $this->_relationships[$k] = $model;
1412
1413
        return $this;
1414
    }
1415
1416
    /**
1417
     * @deprecated
1418
     *
1419
     * Sets the model for a one-to-many relationship
1420
     *
1421
     * @return $this
1422
     */
1423
    public function setRelationCollection(string $k, iterable $models)
1424
    {
1425
        $this->_relationships[$k] = $models;
1426
1427
        return $this;
1428
    }
1429
1430
    /**
1431
     * @deprecated
1432
     *
1433
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null
1434
     *
1435
     * @return $this
1436
     */
1437
    public function clearRelation(string $k)
1438
    {
1439
        $this->$k = null;
1440
        $this->_relationships[$k] = null;
1441
1442
        return $this;
1443
    }
1444
1445
    /////////////////////////////
1446
    // Events
1447
    /////////////////////////////
1448
1449
    /**
1450
     * Adds a listener to the model.creating and model.updating events.
1451
     */
1452
    public static function saving(callable $listener, int $priority = 0): void
1453
    {
1454
        EventManager::listen(static::class, ModelCreating::NAME, $listener, $priority);
1455
        EventManager::listen(static::class, ModelUpdating::NAME, $listener, $priority);
1456
    }
1457
1458
    /**
1459
     * Adds a listener to the model.created and model.updated events.
1460
     */
1461
    public static function saved(callable $listener, int $priority = 0): void
1462
    {
1463
        EventManager::listen(static::class, ModelCreated::NAME, $listener, $priority);
1464
        EventManager::listen(static::class, ModelUpdated::NAME, $listener, $priority);
1465
    }
1466
1467
    /**
1468
     * Adds a listener to the model.creating, model.updating, and model.deleting events.
1469
     */
1470
    public static function beforePersist(callable $listener, int $priority = 0): void
1471
    {
1472
        EventManager::listen(static::class, ModelCreating::NAME, $listener, $priority);
1473
        EventManager::listen(static::class, ModelUpdating::NAME, $listener, $priority);
1474
        EventManager::listen(static::class, ModelDeleting::NAME, $listener, $priority);
1475
    }
1476
1477
    /**
1478
     * Adds a listener to the model.created, model.updated, and model.deleted events.
1479
     */
1480
    public static function afterPersist(callable $listener, int $priority = 0): void
1481
    {
1482
        EventManager::listen(static::class, ModelCreated::NAME, $listener, $priority);
1483
        EventManager::listen(static::class, ModelUpdated::NAME, $listener, $priority);
1484
        EventManager::listen(static::class, ModelDeleted::NAME, $listener, $priority);
1485
    }
1486
1487
    /**
1488
     * Adds a listener to the model.creating event.
1489
     */
1490
    public static function creating(callable $listener, int $priority = 0): void
1491
    {
1492
        EventManager::listen(static::class, ModelCreating::NAME, $listener, $priority);
1493
    }
1494
1495
    /**
1496
     * Adds a listener to the model.created event.
1497
     */
1498
    public static function created(callable $listener, int $priority = 0): void
1499
    {
1500
        EventManager::listen(static::class, ModelCreated::NAME, $listener, $priority);
1501
    }
1502
1503
    /**
1504
     * Adds a listener to the model.updating event.
1505
     */
1506
    public static function updating(callable $listener, int $priority = 0): void
1507
    {
1508
        EventManager::listen(static::class, ModelUpdating::NAME, $listener, $priority);
1509
    }
1510
1511
    /**
1512
     * Adds a listener to the model.updated event.
1513
     */
1514
    public static function updated(callable $listener, int $priority = 0): void
1515
    {
1516
        EventManager::listen(static::class, ModelUpdated::NAME, $listener, $priority);
1517
    }
1518
1519
    /**
1520
     * Adds a listener to the model.deleting event.
1521
     */
1522
    public static function deleting(callable $listener, int $priority = 0): void
1523
    {
1524
        EventManager::listen(static::class, ModelDeleting::NAME, $listener, $priority);
1525
    }
1526
1527
    /**
1528
     * Adds a listener to the model.deleted event.
1529
     */
1530
    public static function deleted(callable $listener, int $priority = 0): void
1531
    {
1532
        EventManager::listen(static::class, ModelDeleted::NAME, $listener, $priority);
1533
    }
1534
1535
    /////////////////////////////
1536
    // Validation
1537
    /////////////////////////////
1538
1539
    /**
1540
     * Gets the error stack for this model.
1541
     */
1542
    public function getErrors(): Errors
1543
    {
1544
        if (!$this->errors) {
1545
            $this->errors = new Errors();
1546
        }
1547
1548
        return $this->errors;
1549
    }
1550
1551
    /**
1552
     * Checks if the model in its current state is valid.
1553
     */
1554
    public function valid(): bool
1555
    {
1556
        // clear any previous errors
1557
        $this->getErrors()->clear();
1558
1559
        // run the validator against the unsaved model values
1560
        $validated = true;
1561
        foreach ($this->_unsaved as $k => &$v) {
1562
            $property = static::definition()->get($k);
1563
            $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 1562 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...
1564
        }
1565
1566
        return $validated;
1567
    }
1568
}
1569