Completed
Push — master ( 42c024...4d45bc )
by Jared
01:47
created

Model::getRelationship()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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