Completed
Push — master ( 75aba3...58dc52 )
by Jared
01:34
created

Model::getPropertyTitle()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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