Completed
Push — master ( 08c9ff...91f666 )
by Jared
01:35
created

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

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

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

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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