Completed
Push — master ( cf76cc...aeebc0 )
by Jared
01:28
created

Model::offsetUnset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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