Completed
Push — master ( 1cb542...8b3d70 )
by Jared
01:40
created

Model::buildDefinition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
177
            }
178
179
            $ids[$name] = $id;
180
            $this->hasId = $this->hasId && $id;
181
        }
182
183
        $this->idValues = $ids;
184
185
        // load any given values
186
        if ($this->hasId && count($values) > count($ids)) {
187
            $this->refreshWith($values);
188
        } elseif (!$this->hasId) {
189
            $this->_unsaved = $values;
190
        } else {
191
            $this->_values = $this->idValues;
192
        }
193
    }
194
195
    /**
196
     * Performs initialization on this model.
197
     */
198
    private function init()
199
    {
200
        // ensure the initialize function is called only once
201
        $k = static::class;
202
        if (!isset(self::$initialized[$k])) {
203
            $this->initialize();
204
            self::$initialized[$k] = true;
205
        }
206
    }
207
208
    /**
209
     * The initialize() method is called once per model. This is a great
210
     * place to install event listeners. Any methods on the model that have
211
     * "autoInitialize" in the name will automatically be called.
212
     */
213
    protected function initialize()
214
    {
215
        // Use reflection to automatically call any method here that has a name
216
        // that starts with "autoInitialize". This is useful for traits to install listeners.
217
        $methods = get_class_methods(static::class);
218
        foreach ($methods as $method) {
219
            if (0 === strpos($method, 'autoInitialize')) {
220
                $this->$method();
221
            }
222
        }
223
    }
224
225
    /**
226
     * Sets the driver for all models.
227
     */
228
    public static function setDriver(DriverInterface $driver)
229
    {
230
        self::$driver = $driver;
231
    }
232
233
    /**
234
     * Gets the driver for all models.
235
     *
236
     * @throws DriverMissingException when a driver has not been set yet
237
     */
238
    public static function getDriver(): DriverInterface
239
    {
240
        if (!self::$driver) {
241
            throw new DriverMissingException('A model driver has not been set yet.');
242
        }
243
244
        return self::$driver;
245
    }
246
247
    /**
248
     * Clears the driver for all models.
249
     */
250
    public static function clearDriver()
251
    {
252
        self::$driver = null;
253
    }
254
255
    /**
256
     * Gets the name of the model, i.e. User.
257
     */
258
    public static function modelName(): string
259
    {
260
        // strip namespacing
261
        $paths = explode('\\', static::class);
262
263
        return end($paths);
264
    }
265
266
    /**
267
     * Gets the model ID.
268
     *
269
     * @return string|number|false ID
270
     */
271
    public function id()
272
    {
273
        if (!$this->hasId) {
274
            return false;
275
        }
276
277
        if (1 == count($this->idValues)) {
278
            return reset($this->idValues);
279
        }
280
281
        $result = [];
282
        foreach (static::$ids as $k) {
283
            $result[] = $this->idValues[$k];
284
        }
285
286
        return implode(',', $result);
287
    }
288
289
    /**
290
     * Gets a key-value map of the model ID.
291
     *
292
     * @return array ID map
293
     */
294
    public function ids(): array
295
    {
296
        return $this->idValues;
297
    }
298
299
    /**
300
     * Checks if the model has an identifier present.
301
     * This does not indicate whether the model has been
302
     * persisted to the database or loaded from the database.
303
     */
304
    public function hasId(): bool
305
    {
306
        return $this->hasId;
307
    }
308
309
    /////////////////////////////
310
    // Magic Methods
311
    /////////////////////////////
312
313
    /**
314
     * Converts the model into a string.
315
     *
316
     * @return string
317
     */
318
    public function __toString()
319
    {
320
        $values = array_merge($this->_values, $this->_unsaved, $this->idValues);
321
        ksort($values);
322
323
        return static::class.'('.json_encode($values, JSON_PRETTY_PRINT).')';
324
    }
325
326
    /**
327
     * Shortcut to a get() call for a given property.
328
     *
329
     * @param string $name
330
     *
331
     * @return mixed
332
     */
333
    public function __get($name)
334
    {
335
        $result = $this->get([$name]);
336
337
        return reset($result);
338
    }
339
340
    /**
341
     * Sets an unsaved value.
342
     *
343
     * @param string $name
344
     * @param mixed  $value
345
     */
346
    public function __set($name, $value)
347
    {
348
        // if changing property, remove relation model
349
        if (isset($this->_relationships[$name])) {
350
            unset($this->_relationships[$name]);
351
        }
352
353
        // call any mutators
354
        $mutator = self::getMutator($name);
355
        if ($mutator) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mutator of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

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