Completed
Push — master ( 1bf18c...56d3b9 )
by Jared
02:30
created

Model::buildDefinition()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
177
            }
178
179
            $ids[$name] = $id;
180
            $this->hasId = $this->hasId && $id;
181
        }
182
183
        $this->idValues = $ids;
184
185
        // load any given values
186
        if ($this->hasId && count($values) > count($ids)) {
187
            $this->refreshWith($values);
188
        } elseif (!$this->hasId) {
189
            $this->_unsaved = $values;
190
        } else {
191
            $this->_values = $this->idValues;
192
        }
193
    }
194
195
    /**
196
     * Performs initialization on this model.
197
     */
198
    private function init()
199
    {
200
        // ensure the initialize function is called only once
201
        $k = static::class;
202
        if (!isset(self::$initialized[$k])) {
203
            $this->initialize();
204
            self::$initialized[$k] = true;
205
        }
206
    }
207
208
    /**
209
     * The initialize() method is called once per model. This is a great
210
     * place to install event listeners. Any methods on the model that have
211
     * "autoInitialize" in the name will automatically be called.
212
     */
213
    protected function initialize()
214
    {
215
        // Use reflection to automatically call any method here that has a name
216
        // that starts with "autoInitialize". This is useful for traits to install listeners.
217
        $methods = get_class_methods(static::class);
218
        foreach ($methods as $method) {
219
            if (0 === strpos($method, 'autoInitialize')) {
220
                $this->$method();
221
            }
222
        }
223
    }
224
225
    /**
226
     * Sets the driver for all models.
227
     */
228
    public static function setDriver(DriverInterface $driver)
229
    {
230
        self::$driver = $driver;
231
    }
232
233
    /**
234
     * Gets the driver for all models.
235
     *
236
     * @throws DriverMissingException when a driver has not been set yet
237
     */
238
    public static function getDriver(): DriverInterface
239
    {
240
        if (!self::$driver) {
241
            throw new DriverMissingException('A model driver has not been set yet.');
242
        }
243
244
        return self::$driver;
245
    }
246
247
    /**
248
     * Clears the driver for all models.
249
     */
250
    public static function clearDriver()
251
    {
252
        self::$driver = null;
253
    }
254
255
    /**
256
     * Gets the name of the model, i.e. User.
257
     */
258
    public static function modelName(): string
259
    {
260
        // strip namespacing
261
        $paths = explode('\\', static::class);
262
263
        return end($paths);
264
    }
265
266
    /**
267
     * Gets the model ID.
268
     *
269
     * @return string|number|false ID
270
     */
271
    public function id()
272
    {
273
        if (!$this->hasId) {
274
            return false;
275
        }
276
277
        if (1 == count($this->idValues)) {
278
            return reset($this->idValues);
279
        }
280
281
        $result = [];
282
        foreach (static::$ids as $k) {
283
            $result[] = $this->idValues[$k];
284
        }
285
286
        return implode(',', $result);
287
    }
288
289
    /**
290
     * Gets a key-value map of the model ID.
291
     *
292
     * @return array ID map
293
     */
294
    public function ids(): array
295
    {
296
        return $this->idValues;
297
    }
298
299
    /**
300
     * Checks if the model has an identifier present.
301
     * This does not indicate whether the model has been
302
     * persisted to the database or loaded from the database.
303
     */
304
    public function hasId(): bool
305
    {
306
        return $this->hasId;
307
    }
308
309
    /////////////////////////////
310
    // Magic Methods
311
    /////////////////////////////
312
313
    /**
314
     * Converts the model into a string.
315
     *
316
     * @return string
317
     */
318
    public function __toString()
319
    {
320
        $values = array_merge($this->_values, $this->_unsaved, $this->idValues);
321
        ksort($values);
322
323
        return static::class.'('.json_encode($values, JSON_PRETTY_PRINT).')';
324
    }
325
326
    /**
327
     * Shortcut to a get() call for a given property.
328
     *
329
     * @param string $name
330
     *
331
     * @return mixed
332
     */
333
    public function __get($name)
334
    {
335
        $result = $this->get([$name]);
336
337
        return reset($result);
338
    }
339
340
    /**
341
     * Sets an unsaved value.
342
     *
343
     * @param string $name
344
     * @param mixed  $value
345
     */
346
    public function __set($name, $value)
347
    {
348
        // if changing property, remove relation model
349
        if (isset($this->_relationships[$name])) {
350
            unset($this->_relationships[$name]);
351
        }
352
353
        // call any mutators
354
        $mutator = self::getMutator($name);
355
        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...
356
            $this->_unsaved[$name] = $this->$mutator($value);
357
        } else {
358
            $this->_unsaved[$name] = $value;
359
        }
360
361
        // set local ID property on belongs_to relationship
362
        if (static::definition()->has($name)) {
363
            $property = static::definition()->get($name);
364
            if (Relationship::BELONGS_TO == $property->getRelationshipType() && !$property->isPersisted()) {
365
                if ($value instanceof self) {
366
                    $this->_unsaved[$property->getLocalKey()] = $value->{$property->getForeignKey()};
367
                } elseif (null === $value) {
368
                    $this->_unsaved[$property->getLocalKey()] = null;
369
                } else {
370
                    throw new ModelException('The value set on the "'.$name.'" property must be a model or null.');
371
                }
372
            }
373
        }
374
    }
375
376
    /**
377
     * Checks if an unsaved value or property exists by this name.
378
     *
379
     * @param string $name
380
     *
381
     * @return bool
382
     */
383
    public function __isset($name)
384
    {
385
        // isset() must return true for any value that could be returned by offsetGet
386
        // because many callers will first check isset() to see if the value is accessible.
387
        // This method is not supposed to only be valid for unsaved values, or properties
388
        // that have a value.
389
        return array_key_exists($name, $this->_unsaved) || static::definition()->has($name);
390
    }
391
392
    /**
393
     * Unsets an unsaved value.
394
     *
395
     * @param string $name
396
     */
397
    public function __unset($name)
398
    {
399
        if (array_key_exists($name, $this->_unsaved)) {
400
            // if changing property, remove relation model
401
            if (isset($this->_relationships[$name])) {
402
                unset($this->_relationships[$name]);
403
            }
404
405
            unset($this->_unsaved[$name]);
406
        }
407
    }
408
409
    /////////////////////////////
410
    // ArrayAccess Interface
411
    /////////////////////////////
412
413
    public function offsetExists($offset)
414
    {
415
        return isset($this->$offset);
416
    }
417
418
    public function offsetGet($offset)
419
    {
420
        return $this->$offset;
421
    }
422
423
    public function offsetSet($offset, $value)
424
    {
425
        $this->$offset = $value;
426
    }
427
428
    public function offsetUnset($offset)
429
    {
430
        unset($this->$offset);
431
    }
432
433
    public static function __callStatic($name, $parameters)
434
    {
435
        // Any calls to unkown static methods should be deferred to
436
        // the query. This allows calls like User::where()
437
        // to replace User::query()->where().
438
        return call_user_func_array([static::query(), $name], $parameters);
439
    }
440
441
    /////////////////////////////
442
    // Property Definitions
443
    /////////////////////////////
444
445
    /**
446
     * Gets the model definition.
447
     */
448
    public static function definition(): Definition
449
    {
450
        return DefinitionBuilder::get(static::class);
451
    }
452
453
    /**
454
     * The buildDefinition() method is called once per model. It's used
455
     * to generate the model definition. This is a great place to add any
456
     * dynamic model properties.
457
     */
458
    public static function buildDefinition(): Definition
459
    {
460
        // Use reflection to automatically call any method on the model that has a name
461
        // that starts with "buildDefinition". This is useful for traits to add properties.
462
        $methods = get_class_methods(static::class);
463
        foreach ($methods as $method) {
464
            if (0 === strpos($method, 'autoDefinition')) {
465
                static::$method();
466
            }
467
        }
468
469
        // Deprecated: this setting is deprecated
470
        // remove any hidden properties
471
        if (property_exists(static::class, 'hidden')) {
472
            foreach (static::$hidden as $k) {
473
                if (isset(static::$properties[$k])) {
474
                    static::$properties[$k]['in_array'] = false;
475
                }
476
            }
477
        }
478
479
        // Deprecated: this setting is deprecated
480
        // add any appended properties
481
        if (property_exists(static::class, 'appended')) {
482
            foreach (static::$appended as $k) {
483
                static::$properties[$k] = [
484
                    'in_array' => true,
485
                    'persisted' => false,
486
                ];
487
            }
488
        }
489
490
        return DefinitionBuilder::build(static::$properties, static::class);
491
    }
492
493
    /**
494
     * Gets the names of the model ID properties.
495
     */
496
    public static function getIDProperties(): array
497
    {
498
        return static::$ids;
499
    }
500
501
    /**
502
     * Gets the mutator method name for a given property name.
503
     * Looks for methods in the form of `setPropertyValue`.
504
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
505
     *
506
     * @param string $property property
507
     *
508
     * @return string|null method name if it exists
509
     */
510
    public static function getMutator(string $property): ?string
511
    {
512
        $class = static::class;
513
514
        $k = $class.':'.$property;
515
        if (!array_key_exists($k, self::$mutators)) {
516
            $inflector = Inflector::get();
517
            $method = 'set'.$inflector->camelize($property).'Value';
518
519
            if (!method_exists($class, $method)) {
520
                $method = null;
521
            }
522
523
            self::$mutators[$k] = $method;
524
        }
525
526
        return self::$mutators[$k];
527
    }
528
529
    /**
530
     * Gets the accessor method name for a given property name.
531
     * Looks for methods in the form of `getPropertyValue`.
532
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
533
     *
534
     * @param string $property property
535
     *
536
     * @return string|null method name if it exists
537
     */
538
    public static function getAccessor(string $property): ?string
539
    {
540
        $class = static::class;
541
542
        $k = $class.':'.$property;
543
        if (!array_key_exists($k, self::$accessors)) {
544
            $inflector = Inflector::get();
545
            $method = 'get'.$inflector->camelize($property).'Value';
546
547
            if (!method_exists($class, $method)) {
548
                $method = null;
549
            }
550
551
            self::$accessors[$k] = $method;
552
        }
553
554
        return self::$accessors[$k];
555
    }
556
557
    /////////////////////////////
558
    // CRUD Operations
559
    /////////////////////////////
560
561
    /**
562
     * Gets the table name for storing this model.
563
     */
564
    public function getTablename(): string
565
    {
566
        if (!$this->tablename) {
567
            $inflector = Inflector::get();
568
569
            $this->tablename = $inflector->camelize($inflector->pluralize(static::modelName()));
570
        }
571
572
        return $this->tablename;
573
    }
574
575
    /**
576
     * Gets the ID of the connection in the connection manager
577
     * that stores this model.
578
     */
579
    public function getConnection(): ?string
580
    {
581
        return null;
582
    }
583
584
    protected function usesTransactions(): bool
585
    {
586
        return false;
587
    }
588
589
    /**
590
     * Saves the model.
591
     *
592
     * @return bool true when the operation was successful
593
     */
594
    public function save(): bool
595
    {
596
        if (!$this->hasId) {
597
            return $this->create();
598
        }
599
600
        return $this->set();
601
    }
602
603
    /**
604
     * Saves the model. Throws an exception when the operation fails.
605
     *
606
     * @throws ModelException when the model cannot be saved
607
     */
608
    public function saveOrFail()
609
    {
610
        if (!$this->save()) {
611
            $msg = 'Failed to save '.static::modelName();
612
            if ($validationErrors = $this->getErrors()->all()) {
613
                $msg .= ': '.implode(', ', $validationErrors);
614
            }
615
616
            throw new ModelException($msg);
617
        }
618
    }
619
620
    /**
621
     * Creates a new model.
622
     *
623
     * @param array $data optional key-value properties to set
624
     *
625
     * @return bool true when the operation was successful
626
     *
627
     * @throws BadMethodCallException when called on an existing model
628
     */
629
    public function create(array $data = []): bool
630
    {
631
        if ($this->hasId) {
632
            throw new BadMethodCallException('Cannot call create() on an existing model');
633
        }
634
635
        // mass assign values passed into create()
636
        $this->setValues($data);
637
638
        // clear any previous errors
639
        $this->getErrors()->clear();
640
641
        // start a DB transaction if needed
642
        $usesTransactions = $this->usesTransactions();
643
        if ($usesTransactions) {
644
            self::$driver->startTransaction($this->getConnection());
645
        }
646
647
        // dispatch the model.creating event
648
        if (!$this->performDispatch(new ModelCreating($this), $usesTransactions)) {
649
            return false;
650
        }
651
652
        $requiredProperties = [];
653
        foreach (static::definition()->all() as $name => $property) {
654
            // build a list of the required properties
655
            if ($property->isRequired()) {
656
                $requiredProperties[] = $property;
657
            }
658
659
            // add in default values
660
            if (!array_key_exists($name, $this->_unsaved) && $property->hasDefault()) {
661
                $this->_unsaved[$name] = $property->getDefault();
662
            }
663
        }
664
665
        // save any relationships
666
        if (!$this->saveRelationships($usesTransactions)) {
667
            return false;
668
        }
669
670
        // validate the values being saved
671
        $validated = true;
672
        $insertArray = [];
673
        $preservedValues = [];
674
        foreach ($this->_unsaved as $name => $value) {
675
            // exclude if value does not map to a property
676
            $property = static::definition()->get($name);
677
            if (!$property) {
678
                continue;
679
            }
680
681
            // check if this property is persisted to the DB
682
            if (!$property->isPersisted()) {
683
                $preservedValues[$name] = $value;
684
                continue;
685
            }
686
687
            // cannot insert immutable values
688
            // (unless using the default value)
689
            if ($property->isImmutable() && $value !== $property->getDefault()) {
690
                continue;
691
            }
692
693
            $validated = $validated && Validator::validateProperty($this, $property, $value);
694
            $insertArray[$name] = $value;
695
        }
696
697
        // check for required fields
698
        foreach ($requiredProperties as $property) {
699
            $name = $property->getName();
700
            if (!isset($insertArray[$name]) && !isset($preservedValues[$name])) {
701
                $context = [
702
                    'field' => $name,
703
                    'field_name' => $property->getTitle($this),
704
                ];
705
                $this->getErrors()->add('pulsar.validation.required', $context);
706
707
                $validated = false;
708
            }
709
        }
710
711
        if (!$validated) {
712
            // when validations fail roll back any database transaction
713
            if ($usesTransactions) {
714
                self::$driver->rollBackTransaction($this->getConnection());
715
            }
716
717
            return false;
718
        }
719
720
        $created = self::$driver->createModel($this, $insertArray);
721
722
        if ($created) {
723
            // determine the model's new ID
724
            $this->getNewId();
725
726
            // store the persisted values to the in-memory cache
727
            $this->_unsaved = [];
728
            $hydrateValues = array_replace($this->idValues, $preservedValues);
729
730
            // only type-cast the values that were converted to the database format
731
            foreach ($insertArray as $k => $v) {
732
                if ($property = static::definition()->get($k)) {
733
                    $hydrateValues[$k] = Type::cast($property, $v);
734
                } else {
735
                    $hydrateValues[$k] = $v;
736
                }
737
            }
738
            $this->refreshWith($hydrateValues);
739
740
            // dispatch the model.created event
741
            if (!$this->performDispatch(new ModelCreated($this), $usesTransactions)) {
742
                return false;
743
            }
744
        }
745
746
        // commit the transaction, if used
747
        if ($usesTransactions) {
748
            self::$driver->commitTransaction($this->getConnection());
749
        }
750
751
        return $created;
752
    }
753
754
    /**
755
     * Ignores unsaved values when fetching the next value.
756
     *
757
     * @return $this
758
     */
759
    public function ignoreUnsaved()
760
    {
761
        $this->ignoreUnsaved = true;
762
763
        return $this;
764
    }
765
766
    /**
767
     * Fetches property values from the model.
768
     *
769
     * This method looks up values in this order:
770
     * IDs, local cache, unsaved values, storage layer, defaults
771
     *
772
     * @param array $properties list of property names to fetch values of
773
     */
774
    public function get(array $properties): array
775
    {
776
        // check if unsaved values will be returned
777
        $ignoreUnsaved = $this->ignoreUnsaved;
778
        $this->ignoreUnsaved = false;
779
780
        // Check if the model needs to be loaded from the database. This
781
        // is used if an ID was supplied for the model but the values have
782
        // not been hydrated from the database. We only want to load values
783
        // from the database if there are properties requested that are both
784
        // persisted to the database AND do not already have a value present.
785
        $this->loadIfNeeded($properties, $ignoreUnsaved);
786
787
        // build a key-value map of the requested properties
788
        $return = [];
789
        foreach ($properties as $k) {
790
            $return[$k] = $this->getValue($k, $ignoreUnsaved);
791
        }
792
793
        return $return;
794
    }
795
796
    /**
797
     * Loads the model from the database if needed.
798
     */
799
    private function loadIfNeeded(array $properties, bool $ignoreUnsaved): void
800
    {
801
        if ($this->loaded | !$this->hasId) {
802
            return;
803
        }
804
805
        foreach ($properties as $k) {
806
            if (!isset($this->_values[$k]) && ($ignoreUnsaved || !isset($this->_unsaved[$k]))) {
807
                $property = static::definition()->get($k);
808
                if ($property && $property->isPersisted()) {
809
                    $this->refresh();
810
811
                    return;
812
                }
813
            }
814
        }
815
    }
816
817
    /**
818
     * Gets a property value from the model.
819
     *
820
     * Values are looked up in this order:
821
     *  1. unsaved values
822
     *  2. local values
823
     *  3. default value
824
     *  4. null
825
     *
826
     * @return mixed
827
     */
828
    private function getValue(string $name, bool $ignoreUnsaved)
829
    {
830
        $value = null;
831
        if (!$ignoreUnsaved && array_key_exists($name, $this->_unsaved)) {
832
            $value = $this->_unsaved[$name];
833
        } elseif (array_key_exists($name, $this->_values)) {
834
            $value = $this->_values[$name];
835
        } elseif ($property = static::definition()->get($name)) {
836
            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...
837
                $relationship = $this->getRelationship($property);
838
                $value = $this->_values[$name] = $relationship->getResults();
839
            } else {
840
                $value = $this->_values[$name] = $property->getDefault();
841
            }
842
        }
843
844
        // call any accessors
845
        if ($accessor = self::getAccessor($name)) {
846
            $value = $this->$accessor($value);
847
        }
848
849
        return $value;
850
    }
851
852
    /**
853
     * Populates a newly created model with its ID.
854
     */
855
    private function getNewId()
856
    {
857
        $ids = [];
858
        $namedIds = [];
859
        foreach (static::$ids as $k) {
860
            // attempt use the supplied value if the ID property is mutable
861
            $property = static::definition()->get($k);
862
            if (!$property->isImmutable() && isset($this->_unsaved[$k])) {
863
                $id = $this->_unsaved[$k];
864
            } else {
865
                // type-cast the value because it came from the database
866
                $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 861 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...
867
            }
868
869
            $ids[] = $id;
870
            $namedIds[$k] = $id;
871
        }
872
873
        $this->hasId = true;
874
        $this->idValues = $namedIds;
875
        $this->_values = array_replace($this->_values, $namedIds);
876
    }
877
878
    protected function getMassAssignmentWhitelist(): ?array
879
    {
880
        // Deprecated: this is deprecated
881
        if (property_exists($this, 'permitted')) {
882
            return static::$permitted;
883
        }
884
885
        return null;
886
    }
887
888
    protected function getMassAssignmentBlacklist(): ?array
889
    {
890
        // Deprecated: this is deprecated
891
        if (property_exists($this, 'protected')) {
892
            return static::$protected;
893
        }
894
895
        return null;
896
    }
897
898
    /**
899
     * Sets a collection values on the model from an untrusted input.
900
     *
901
     * @param array $values
902
     *
903
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
904
     *
905
     * @return $this
906
     */
907
    public function setValues($values)
908
    {
909
        if ($permitted = $this->getMassAssignmentWhitelist()) {
910
            // use a mass assignment whitelist
911
            foreach ($values as $k => $value) {
912
                // check for mass assignment violations
913
                if (!in_array($k, $permitted)) {
914
                    throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
915
                }
916
917
                $this->$k = $value;
918
            }
919
        } elseif ($protected = $this->getMassAssignmentBlacklist()) {
920
            // use a mass assignment blacklist
921
            foreach ($values as $k => $value) {
922
                // check for mass assignment violations
923
                if (in_array($k, $protected)) {
924
                    throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
925
                }
926
927
                $this->$k = $value;
928
            }
929
        } else {
930
            // no mass assignment protection enabled
931
            foreach ($values as $k => $value) {
932
                $this->$k = $value;
933
            }
934
        }
935
936
        return $this;
937
    }
938
939
    /**
940
     * Converts the model to an array.
941
     */
942
    public function toArray(): array
943
    {
944
        // build the list of properties to return
945
        $properties = [];
946
        foreach (static::definition()->all() as $property) {
947
            if ($property->isInArray()) {
948
                $properties[] = $property->getName();
949
            }
950
        }
951
952
        // get the values for the properties
953
        $result = $this->get($properties);
954
955
        foreach ($result as $k => &$value) {
956
            // convert arrays of models to arrays
957
            if (is_array($value)) {
958
                foreach ($value as &$subValue) {
959
                    if ($subValue instanceof Model) {
960
                        $subValue = $subValue->toArray();
961
                    }
962
                }
963
            }
964
965
            // convert any models to arrays
966
            if ($value instanceof self) {
967
                $value = $value->toArray();
968
            }
969
        }
970
971
        return $result;
972
    }
973
974
    /**
975
     * Checks if the unsaved value for a property is present and
976
     * is different from the original value.
977
     *
978
     * @property string|null $name
979
     * @property bool        $hasChanged when true, checks if the unsaved value is different from the saved value
980
     */
981
    public function dirty(?string $name = null, bool $hasChanged = false): bool
982
    {
983
        if (!$name) {
984
            if ($hasChanged) {
985
                throw new \RuntimeException('Checking if all properties have changed is not supported');
986
            }
987
988
            return count($this->_unsaved) > 0;
989
        }
990
991
        if (!array_key_exists($name, $this->_unsaved)) {
992
            return false;
993
        }
994
995
        if (!$hasChanged) {
996
            return true;
997
        }
998
999
        return $this->$name !== $this->ignoreUnsaved()->$name;
1000
    }
1001
1002
    /**
1003
     * Updates the model.
1004
     *
1005
     * @param array $data optional key-value properties to set
1006
     *
1007
     * @return bool true when the operation was successful
1008
     *
1009
     * @throws BadMethodCallException when not called on an existing model
1010
     */
1011
    public function set(array $data = []): bool
1012
    {
1013
        if (!$this->hasId) {
1014
            throw new BadMethodCallException('Can only call set() on an existing model');
1015
        }
1016
1017
        // mass assign values passed into set()
1018
        $this->setValues($data);
1019
1020
        // clear any previous errors
1021
        $this->getErrors()->clear();
1022
1023
        // not updating anything?
1024
        if (0 == count($this->_unsaved)) {
1025
            return true;
1026
        }
1027
1028
        // start a DB transaction if needed
1029
        $usesTransactions = $this->usesTransactions();
1030
        if ($usesTransactions) {
1031
            self::$driver->startTransaction($this->getConnection());
1032
        }
1033
1034
        // dispatch the model.updating event
1035
        if (!$this->performDispatch(new ModelUpdating($this), $usesTransactions)) {
1036
            return false;
1037
        }
1038
1039
        // save any relationships
1040
        if (!$this->saveRelationships($usesTransactions)) {
1041
            return false;
1042
        }
1043
1044
        // validate the values being saved
1045
        $validated = true;
1046
        $updateArray = [];
1047
        $preservedValues = [];
1048
        foreach ($this->_unsaved as $name => $value) {
1049
            // exclude if value does not map to a property
1050
            if (!static::definition()->has($name)) {
1051
                continue;
1052
            }
1053
1054
            $property = static::definition()->get($name);
1055
1056
            // check if this property is persisted to the DB
1057
            if (!$property->isPersisted()) {
1058
                $preservedValues[$name] = $value;
1059
                continue;
1060
            }
1061
1062
            // can only modify mutable properties
1063
            if (!$property->isMutable()) {
1064
                continue;
1065
            }
1066
1067
            $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 1054 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...
1068
            $updateArray[$name] = $value;
1069
        }
1070
1071
        if (!$validated) {
1072
            // when validations fail roll back any database transaction
1073
            if ($usesTransactions) {
1074
                self::$driver->rollBackTransaction($this->getConnection());
1075
            }
1076
1077
            return false;
1078
        }
1079
1080
        $updated = self::$driver->updateModel($this, $updateArray);
1081
1082
        if ($updated) {
1083
            // store the persisted values to the in-memory cache
1084
            $this->_unsaved = [];
1085
            $hydrateValues = array_replace($this->_values, $preservedValues);
1086
1087
            // only type-cast the values that were converted to the database format
1088
            foreach ($updateArray as $k => $v) {
1089
                if ($property = static::definition()->get($k)) {
1090
                    $hydrateValues[$k] = Type::cast($property, $v);
1091
                } else {
1092
                    $hydrateValues[$k] = $v;
1093
                }
1094
            }
1095
            $this->refreshWith($hydrateValues);
1096
1097
            // dispatch the model.updated event
1098
            if (!$this->performDispatch(new ModelUpdated($this), $usesTransactions)) {
1099
                return false;
1100
            }
1101
        }
1102
1103
        // commit the transaction, if used
1104
        if ($usesTransactions) {
1105
            self::$driver->commitTransaction($this->getConnection());
1106
        }
1107
1108
        return $updated;
1109
    }
1110
1111
    /**
1112
     * Delete the model.
1113
     *
1114
     * @return bool true when the operation was successful
1115
     */
1116
    public function delete(): bool
1117
    {
1118
        if (!$this->hasId) {
1119
            throw new BadMethodCallException('Can only call delete() on an existing model');
1120
        }
1121
1122
        // clear any previous errors
1123
        $this->getErrors()->clear();
1124
1125
        // start a DB transaction if needed
1126
        $usesTransactions = $this->usesTransactions();
1127
        if ($usesTransactions) {
1128
            self::$driver->startTransaction($this->getConnection());
1129
        }
1130
1131
        // dispatch the model.deleting event
1132
        if (!$this->performDispatch(new ModelDeleting($this), $usesTransactions)) {
1133
            return false;
1134
        }
1135
1136
        // perform a hard (default) or soft delete
1137
        $hardDelete = true;
1138
        if (isset($this->deleted_at)) {
1139
            $t = time();
1140
            $this->deleted_at = $t;
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1141
            $t = Validator::validateProperty($this, static::definition()->get('deleted_at'), $t);
0 ignored issues
show
Bug introduced by
It seems like static::definition()->get('deleted_at') can be null; however, validateProperty() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1142
            $deleted = self::$driver->updateModel($this, ['deleted_at' => $t]);
1143
            $hardDelete = false;
1144
        } else {
1145
            $deleted = self::$driver->deleteModel($this);
1146
        }
1147
1148
        if ($deleted) {
1149
            // dispatch the model.deleted event
1150
            if (!$this->performDispatch(new ModelDeleted($this), $usesTransactions)) {
1151
                return false;
1152
            }
1153
1154
            if ($hardDelete) {
1155
                $this->_persisted = false;
1156
            }
1157
        }
1158
1159
        // commit the transaction, if used
1160
        if ($usesTransactions) {
1161
            self::$driver->commitTransaction($this->getConnection());
1162
        }
1163
1164
        return $deleted;
1165
    }
1166
1167
    /**
1168
     * Restores a soft-deleted model.
1169
     */
1170
    public function restore(): bool
1171
    {
1172
        if (!isset($this->deleted_at) || !$this->deleted_at) {
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1173
            throw new BadMethodCallException('Can only call restore() on a soft-deleted model');
1174
        }
1175
1176
        // start a DB transaction if needed
1177
        $usesTransactions = $this->usesTransactions();
1178
        if ($usesTransactions) {
1179
            self::$driver->startTransaction($this->getConnection());
1180
        }
1181
1182
        // dispatch the model.updating event
1183
        if (!$this->performDispatch(new ModelUpdating($this), $usesTransactions)) {
1184
            return false;
1185
        }
1186
1187
        $this->deleted_at = null;
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1188
        $restored = self::$driver->updateModel($this, ['deleted_at' => null]);
1189
1190
        if ($restored) {
1191
            // dispatch the model.updated event
1192
            if (!$this->performDispatch(new ModelUpdated($this), $usesTransactions)) {
1193
                return false;
1194
            }
1195
        }
1196
1197
        // commit the transaction, if used
1198
        if ($usesTransactions) {
1199
            self::$driver->commitTransaction($this->getConnection());
1200
        }
1201
1202
        return $restored;
1203
    }
1204
1205
    /**
1206
     * Checks if the model has been deleted.
1207
     */
1208
    public function isDeleted(): bool
1209
    {
1210
        if (isset($this->deleted_at) && $this->deleted_at) {
0 ignored issues
show
Documentation introduced by
The property deleted_at does not exist on object<Pulsar\Model>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1211
            return true;
1212
        }
1213
1214
        return !$this->_persisted;
1215
    }
1216
1217
    /////////////////////////////
1218
    // Queries
1219
    /////////////////////////////
1220
1221
    /**
1222
     * Generates a new query instance.
1223
     */
1224
    public static function query(): Query
1225
    {
1226
        // Create a new model instance for the query to ensure
1227
        // that the model's initialize() method gets called.
1228
        // Otherwise, the property definitions will be incomplete.
1229
        $model = new static();
1230
        $query = new Query($model);
1231
1232
        // scope soft-deleted models to only include non-deleted models
1233
        if (self::definition()->has('deleted_at')) {
1234
            $query->where('deleted_at IS NOT NULL');
1235
        }
1236
1237
        return $query;
1238
    }
1239
1240
    /**
1241
     * Generates a new query instance that includes soft-deleted models.
1242
     */
1243
    public static function withDeleted(): Query
1244
    {
1245
        // Create a new model instance for the query to ensure
1246
        // that the model's initialize() method gets called.
1247
        // Otherwise, the property definitions will be incomplete.
1248
        $model = new static();
1249
1250
        return new Query($model);
1251
    }
1252
1253
    /**
1254
     * Finds a single instance of a model given it's ID.
1255
     *
1256
     * @param mixed $id
1257
     *
1258
     * @return static|null
1259
     */
1260
    public static function find($id): ?self
1261
    {
1262
        $ids = [];
1263
        $id = (array) $id;
1264
        foreach (static::$ids as $j => $k) {
1265
            if (isset($id[$j])) {
1266
                $ids[$k] = $id[$j];
1267
            }
1268
        }
1269
1270
        // malformed ID
1271
        if (count($ids) < count(static::$ids)) {
1272
            return null;
1273
        }
1274
1275
        return static::query()->where($ids)->first();
1276
    }
1277
1278
    /**
1279
     * Finds a single instance of a model given it's ID or throws an exception.
1280
     *
1281
     * @param mixed $id
1282
     *
1283
     * @return static
1284
     *
1285
     * @throws ModelNotFoundException when a model could not be found
1286
     */
1287
    public static function findOrFail($id): self
1288
    {
1289
        $model = static::find($id);
1290
        if (!$model) {
1291
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1292
        }
1293
1294
        return $model;
1295
    }
1296
1297
    /**
1298
     * Tells if this model instance has been persisted to the data layer.
1299
     *
1300
     * NOTE: this does not actually perform a check with the data layer
1301
     */
1302
    public function persisted(): bool
1303
    {
1304
        return $this->_persisted;
1305
    }
1306
1307
    /**
1308
     * Loads the model from the storage layer.
1309
     *
1310
     * @return $this
1311
     */
1312
    public function refresh()
1313
    {
1314
        if (!$this->hasId) {
1315
            return $this;
1316
        }
1317
1318
        $values = self::$driver->loadModel($this);
1319
1320
        if (!is_array($values)) {
1321
            return $this;
1322
        }
1323
1324
        // clear any relations
1325
        $this->_relationships = [];
1326
1327
        // type-cast the values that come from the database
1328
        foreach ($values as $k => &$v) {
1329
            if ($property = static::definition()->get($k)) {
1330
                $v = Type::cast($property, $v);
1331
            }
1332
        }
1333
1334
        return $this->refreshWith($values);
1335
    }
1336
1337
    /**
1338
     * Loads values into the model.
1339
     *
1340
     * @param array $values values
1341
     *
1342
     * @return $this
1343
     */
1344
    public function refreshWith(array $values)
1345
    {
1346
        $this->loaded = true;
1347
        $this->_persisted = true;
1348
        $this->_values = $values;
1349
1350
        return $this;
1351
    }
1352
1353
    /**
1354
     * Clears the cache for this model.
1355
     *
1356
     * @return $this
1357
     */
1358
    public function clearCache()
1359
    {
1360
        $this->loaded = false;
1361
        $this->_unsaved = [];
1362
        $this->_values = [];
1363
        $this->_relationships = [];
1364
1365
        return $this;
1366
    }
1367
1368
    /////////////////////////////
1369
    // Relationships
1370
    /////////////////////////////
1371
1372
    /**
1373
     * Gets the relationship manager for a property.
1374
     *
1375
     * @throws InvalidArgumentException when the relationship manager cannot be created
1376
     */
1377
    private function getRelationship(Property $property): AbstractRelation
1378
    {
1379
        $name = $property->getName();
1380
        if (!isset($this->relationships[$name])) {
1381
            $this->relationships[$name] = Relationship::make($this, $property);
1382
        }
1383
1384
        return $this->relationships[$name];
1385
    }
1386
1387
    /**
1388
     * Saves any unsaved models attached through a relationship. This will only
1389
     * save attached models that have not been saved yet.
1390
     */
1391
    private function saveRelationships(bool $usesTransactions): bool
1392
    {
1393
        try {
1394
            foreach ($this->_unsaved as $k => $value) {
1395
                if ($value instanceof self && !$value->persisted()) {
1396
                    $property = static::definition()->get($k);
1397
                    if ($property && !$property->isPersisted()) {
1398
                        $value->saveOrFail();
1399
                        // set the model again to update any ID properties
1400
                        $this->$k = $value;
1401
                    }
1402
                } elseif (is_array($value)) {
1403
                    foreach ($value as $subValue) {
1404
                        if ($subValue instanceof self && !$subValue->persisted()) {
1405
                            $property = static::definition()->get($k);
1406
                            if ($property && !$property->isPersisted()) {
1407
                                $subValue->saveOrFail();
1408
                            }
1409
                        }
1410
                    }
1411
                }
1412
            }
1413
        } catch (ModelException $e) {
1414
            $this->getErrors()->add($e->getMessage());
1415
1416
            if ($usesTransactions) {
1417
                self::$driver->rollBackTransaction($this->getConnection());
1418
            }
1419
1420
            return false;
1421
        }
1422
1423
        return true;
1424
    }
1425
1426
    /**
1427
     * This hydrates an individual property in the model. It can be a
1428
     * scalar value or relationship.
1429
     *
1430
     * @internal
1431
     *
1432
     * @param $value
1433
     */
1434
    public function hydrateValue(string $name, $value): void
1435
    {
1436
        // type-cast the value because it came from the database
1437
        if ($property = static::definition()->get($name)) {
1438
            $this->_values[$name] = Type::cast($property, $value);
1439
        } else {
1440
            $this->_values[$name] = $value;
1441
        }
1442
    }
1443
1444
    /**
1445
     * @deprecated
1446
     *
1447
     * Gets the model(s) for a relationship
1448
     *
1449
     * @param string $k property
1450
     *
1451
     * @throws InvalidArgumentException when the relationship manager cannot be created
1452
     *
1453
     * @return Model|array|null
1454
     */
1455
    public function relation(string $k)
1456
    {
1457
        if (!array_key_exists($k, $this->_relationships)) {
1458
            $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...
1459
            $this->_relationships[$k] = $relation->getResults();
1460
        }
1461
1462
        return $this->_relationships[$k];
1463
    }
1464
1465
    /**
1466
     * @deprecated
1467
     *
1468
     * Sets the model for a one-to-one relationship (has-one or belongs-to)
1469
     *
1470
     * @return $this
1471
     */
1472
    public function setRelation(string $k, Model $model)
1473
    {
1474
        $this->$k = $model->id();
1475
        $this->_relationships[$k] = $model;
1476
1477
        return $this;
1478
    }
1479
1480
    /**
1481
     * @deprecated
1482
     *
1483
     * Sets the model for a one-to-many relationship
1484
     *
1485
     * @return $this
1486
     */
1487
    public function setRelationCollection(string $k, iterable $models)
1488
    {
1489
        $this->_relationships[$k] = $models;
1490
1491
        return $this;
1492
    }
1493
1494
    /**
1495
     * @deprecated
1496
     *
1497
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null
1498
     *
1499
     * @return $this
1500
     */
1501
    public function clearRelation(string $k)
1502
    {
1503
        $this->$k = null;
1504
        $this->_relationships[$k] = null;
1505
1506
        return $this;
1507
    }
1508
1509
    /////////////////////////////
1510
    // Events
1511
    /////////////////////////////
1512
1513
    /**
1514
     * Gets the event dispatcher.
1515
     */
1516
    public static function getDispatcher($ignoreCache = false): EventDispatcher
1517
    {
1518
        $class = static::class;
1519
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1520
            self::$dispatchers[$class] = new EventDispatcher();
1521
        }
1522
1523
        return self::$dispatchers[$class];
1524
    }
1525
1526
    /**
1527
     * Subscribes to a listener to an event.
1528
     *
1529
     * @param string $event    event name
1530
     * @param int    $priority optional priority, higher #s get called first
1531
     */
1532
    public static function listen(string $event, callable $listener, int $priority = 0)
1533
    {
1534
        static::getDispatcher()->addListener($event, $listener, $priority);
1535
    }
1536
1537
    /**
1538
     * Adds a listener to the model.creating and model.updating events.
1539
     */
1540
    public static function saving(callable $listener, int $priority = 0)
1541
    {
1542
        static::listen(ModelCreating::NAME, $listener, $priority);
1543
        static::listen(ModelUpdating::NAME, $listener, $priority);
1544
    }
1545
1546
    /**
1547
     * Adds a listener to the model.created and model.updated events.
1548
     */
1549
    public static function saved(callable $listener, int $priority = 0)
1550
    {
1551
        static::listen(ModelCreated::NAME, $listener, $priority);
1552
        static::listen(ModelUpdated::NAME, $listener, $priority);
1553
    }
1554
1555
    /**
1556
     * Adds a listener to the model.creating, model.updating, and model.deleting events.
1557
     */
1558
    public static function beforePersist(callable $listener, int $priority = 0)
1559
    {
1560
        static::listen(ModelCreating::NAME, $listener, $priority);
1561
        static::listen(ModelUpdating::NAME, $listener, $priority);
1562
        static::listen(ModelDeleting::NAME, $listener, $priority);
1563
    }
1564
1565
    /**
1566
     * Adds a listener to the model.created, model.updated, and model.deleted events.
1567
     */
1568
    public static function afterPersist(callable $listener, int $priority = 0)
1569
    {
1570
        static::listen(ModelCreated::NAME, $listener, $priority);
1571
        static::listen(ModelUpdated::NAME, $listener, $priority);
1572
        static::listen(ModelDeleted::NAME, $listener, $priority);
1573
    }
1574
1575
    /**
1576
     * Adds a listener to the model.creating event.
1577
     */
1578
    public static function creating(callable $listener, int $priority = 0)
1579
    {
1580
        static::listen(ModelCreating::NAME, $listener, $priority);
1581
    }
1582
1583
    /**
1584
     * Adds a listener to the model.created event.
1585
     */
1586
    public static function created(callable $listener, int $priority = 0)
1587
    {
1588
        static::listen(ModelCreated::NAME, $listener, $priority);
1589
    }
1590
1591
    /**
1592
     * Adds a listener to the model.updating event.
1593
     */
1594
    public static function updating(callable $listener, int $priority = 0)
1595
    {
1596
        static::listen(ModelUpdating::NAME, $listener, $priority);
1597
    }
1598
1599
    /**
1600
     * Adds a listener to the model.updated event.
1601
     */
1602
    public static function updated(callable $listener, int $priority = 0)
1603
    {
1604
        static::listen(ModelUpdated::NAME, $listener, $priority);
1605
    }
1606
1607
    /**
1608
     * Adds a listener to the model.deleting event.
1609
     */
1610
    public static function deleting(callable $listener, int $priority = 0)
1611
    {
1612
        static::listen(ModelDeleting::NAME, $listener, $priority);
1613
    }
1614
1615
    /**
1616
     * Adds a listener to the model.deleted event.
1617
     */
1618
    public static function deleted(callable $listener, int $priority = 0)
1619
    {
1620
        static::listen(ModelDeleted::NAME, $listener, $priority);
1621
    }
1622
1623
    /**
1624
     * Dispatches the given event and checks if it was successful.
1625
     *
1626
     * @return bool true if the events were successfully propagated
1627
     */
1628
    private function performDispatch(AbstractEvent $event, bool $usesTransactions): bool
1629
    {
1630
        static::getDispatcher()->dispatch($event, $event::NAME);
1631
1632
        if (!$event->isPropagationStopped()) {
1633
            return true;
1634
        }
1635
1636
        // when listeners fail roll back any database transaction
1637
        if ($usesTransactions) {
1638
            self::$driver->rollBackTransaction($this->getConnection());
1639
        }
1640
1641
        return false;
1642
    }
1643
1644
    /////////////////////////////
1645
    // Validation
1646
    /////////////////////////////
1647
1648
    /**
1649
     * Gets the error stack for this model.
1650
     */
1651
    public function getErrors(): Errors
1652
    {
1653
        if (!$this->errors) {
1654
            $this->errors = new Errors();
1655
        }
1656
1657
        return $this->errors;
1658
    }
1659
1660
    /**
1661
     * Checks if the model in its current state is valid.
1662
     */
1663
    public function valid(): bool
1664
    {
1665
        // clear any previous errors
1666
        $this->getErrors()->clear();
1667
1668
        // run the validator against the unsaved model values
1669
        $validated = true;
1670
        foreach ($this->_unsaved as $k => &$v) {
1671
            $property = static::definition()->get($k);
1672
            $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 1671 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...
1673
        }
1674
1675
        return $validated;
1676
    }
1677
}
1678