Completed
Push — master ( 2deeb8...4a0110 )
by Jared
02:52 queued 01:17
created

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

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1351
                        }
1352
                    }
1353
                }
1354
            }
1355
        } catch (ModelException $e) {
1356
            $this->getErrors()->add($e->getMessage());
1357
1358
            if ($usesTransactions) {
1359
                self::$driver->rollBackTransaction($this->getConnection());
1360
            }
1361
1362
            return false;
1363
        }
1364
1365
        return true;
1366
    }
1367
1368
    /**
1369
     * This hydrates an individual property in the model. It can be a
1370
     * scalar value or relationship.
1371
     *
1372
     * @internal
1373
     *
1374
     * @param $value
1375
     */
1376
    public function hydrateValue(string $name, $value): void
1377
    {
1378
        if ($property = static::definition()->get($name)) {
1379
            $this->_values[$name] = Type::cast($property, $value);
1380
        } else {
1381
            $this->_values[$name] = $value;
1382
        }
1383
    }
1384
1385
    /**
1386
     * @deprecated
1387
     *
1388
     * Gets the model(s) for a relationship
1389
     *
1390
     * @param string $k property
1391
     *
1392
     * @throws InvalidArgumentException when the relationship manager cannot be created
1393
     *
1394
     * @return Model|array|null
1395
     */
1396
    public function relation(string $k)
1397
    {
1398
        if (!array_key_exists($k, $this->_relationships)) {
1399
            $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...
1400
            $this->_relationships[$k] = $relation->getResults();
1401
        }
1402
1403
        return $this->_relationships[$k];
1404
    }
1405
1406
    /**
1407
     * @deprecated
1408
     *
1409
     * Sets the model for a one-to-one relationship (has-one or belongs-to)
1410
     *
1411
     * @return $this
1412
     */
1413
    public function setRelation(string $k, Model $model)
1414
    {
1415
        $this->$k = $model->id();
1416
        $this->_relationships[$k] = $model;
1417
1418
        return $this;
1419
    }
1420
1421
    /**
1422
     * @deprecated
1423
     *
1424
     * Sets the model for a one-to-many relationship
1425
     *
1426
     * @return $this
1427
     */
1428
    public function setRelationCollection(string $k, iterable $models)
1429
    {
1430
        $this->_relationships[$k] = $models;
1431
1432
        return $this;
1433
    }
1434
1435
    /**
1436
     * @deprecated
1437
     *
1438
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null
1439
     *
1440
     * @return $this
1441
     */
1442
    public function clearRelation(string $k)
1443
    {
1444
        $this->$k = null;
1445
        $this->_relationships[$k] = null;
1446
1447
        return $this;
1448
    }
1449
1450
    /////////////////////////////
1451
    // Events
1452
    /////////////////////////////
1453
1454
    /**
1455
     * Gets the event dispatcher.
1456
     */
1457
    public static function getDispatcher($ignoreCache = false): EventDispatcher
1458
    {
1459
        $class = static::class;
1460
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1461
            self::$dispatchers[$class] = new EventDispatcher();
1462
        }
1463
1464
        return self::$dispatchers[$class];
1465
    }
1466
1467
    /**
1468
     * Subscribes to a listener to an event.
1469
     *
1470
     * @param string $event    event name
1471
     * @param int    $priority optional priority, higher #s get called first
1472
     */
1473
    public static function listen(string $event, callable $listener, int $priority = 0)
1474
    {
1475
        static::getDispatcher()->addListener($event, $listener, $priority);
1476
    }
1477
1478
    /**
1479
     * Adds a listener to the model.creating and model.updating events.
1480
     */
1481
    public static function saving(callable $listener, int $priority = 0)
1482
    {
1483
        static::listen(ModelEvent::CREATING, $listener, $priority);
1484
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1485
    }
1486
1487
    /**
1488
     * Adds a listener to the model.created and model.updated events.
1489
     */
1490
    public static function saved(callable $listener, int $priority = 0)
1491
    {
1492
        static::listen(ModelEvent::CREATED, $listener, $priority);
1493
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1494
    }
1495
1496
    /**
1497
     * Adds a listener to the model.creating event.
1498
     */
1499
    public static function creating(callable $listener, int $priority = 0)
1500
    {
1501
        static::listen(ModelEvent::CREATING, $listener, $priority);
1502
    }
1503
1504
    /**
1505
     * Adds a listener to the model.created event.
1506
     */
1507
    public static function created(callable $listener, int $priority = 0)
1508
    {
1509
        static::listen(ModelEvent::CREATED, $listener, $priority);
1510
    }
1511
1512
    /**
1513
     * Adds a listener to the model.updating event.
1514
     */
1515
    public static function updating(callable $listener, int $priority = 0)
1516
    {
1517
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1518
    }
1519
1520
    /**
1521
     * Adds a listener to the model.updated event.
1522
     */
1523
    public static function updated(callable $listener, int $priority = 0)
1524
    {
1525
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1526
    }
1527
1528
    /**
1529
     * Adds a listener to the model.deleting event.
1530
     */
1531
    public static function deleting(callable $listener, int $priority = 0)
1532
    {
1533
        static::listen(ModelEvent::DELETING, $listener, $priority);
1534
    }
1535
1536
    /**
1537
     * Adds a listener to the model.deleted event.
1538
     */
1539
    public static function deleted(callable $listener, int $priority = 0)
1540
    {
1541
        static::listen(ModelEvent::DELETED, $listener, $priority);
1542
    }
1543
1544
    /**
1545
     * Dispatches the given event and checks if it was successful.
1546
     *
1547
     * @return bool true if the events were successfully propagated
1548
     */
1549
    private function performDispatch(string $eventName, bool $usesTransactions): bool
1550
    {
1551
        $event = new ModelEvent($this);
1552
        static::getDispatcher()->dispatch($event, $eventName);
1553
1554
        // when listeners fail roll back any database transaction
1555
        if ($event->isPropagationStopped()) {
1556
            if ($usesTransactions) {
1557
                self::$driver->rollBackTransaction($this->getConnection());
1558
            }
1559
1560
            return false;
1561
        }
1562
1563
        return true;
1564
    }
1565
1566
    /////////////////////////////
1567
    // Validation
1568
    /////////////////////////////
1569
1570
    /**
1571
     * Gets the error stack for this model.
1572
     */
1573
    public function getErrors(): Errors
1574
    {
1575
        if (!$this->errors) {
1576
            $this->errors = new Errors();
1577
        }
1578
1579
        return $this->errors;
1580
    }
1581
1582
    /**
1583
     * Checks if the model in its current state is valid.
1584
     */
1585
    public function valid(): bool
1586
    {
1587
        // clear any previous errors
1588
        $this->getErrors()->clear();
1589
1590
        // run the validator against the model values
1591
        $values = $this->_unsaved + $this->_values;
1592
1593
        $validated = true;
1594
        foreach ($values as $k => $v) {
1595
            $property = static::definition()->get($k);
1596
            $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 1595 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...
1597
        }
1598
1599
        // add back any modified unsaved values
1600
        foreach (array_keys($this->_unsaved) as $k) {
1601
            $this->_unsaved[$k] = $values[$k];
1602
        }
1603
1604
        return $validated;
1605
    }
1606
}
1607