Completed
Push — master ( 7f60e8...0bb0d8 )
by Jared
01:31
created

Model   F

Complexity

Total Complexity 206

Size/Duplication

Total Lines 1563
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 0
Metric Value
wmc 206
lcom 1
cbo 18
dl 0
loc 1563
rs 0.8
c 0
b 0
f 0

72 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
A init() 0 9 2
A initialize() 0 14 2
B parseId() 0 45 8
A setDriver() 0 4 1
A getDriver() 0 8 2
A clearDriver() 0 4 1
A modelName() 0 7 1
A id() 0 17 4
A ids() 0 4 1
A __toString() 0 7 1
A __get() 0 6 1
B __set() 0 23 6
A __isset() 0 4 2
A __unset() 0 11 3
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A offsetSet() 0 4 1
A offsetUnset() 0 4 1
A __callStatic() 0 7 1
A getProperties() 0 4 1
A buildDefinition() 0 7 1
A getProperty() 0 4 1
A getIDProperties() 0 4 1
A hasProperty() 0 4 1
A getMutator() 0 18 3
A getAccessor() 0 18 3
A getTablename() 0 6 1
A getConnection() 0 4 1
A usesTransactions() 0 4 1
A save() 0 8 2
A saveOrFail() 0 11 3
F create() 0 109 21
A ignoreUnsaved() 0 6 1
B get() 0 38 6
A getValue() 0 17 4
A getNewID() 0 20 4
B setValues() 0 20 9
A toArray() 0 25 5
C set() 0 84 15
B delete() 0 50 9
B restore() 0 34 8
A isDeleted() 0 8 3
A query() 0 15 2
A withDeleted() 0 9 1
A find() 0 17 4
A findOrFail() 0 9 2
A persisted() 0 4 1
A refresh() 0 17 3
A refreshWith() 0 15 3
A clearCache() 0 9 1
A relation() 0 9 2
A setRelation() 0 7 1
A setRelationCollection() 0 6 1
A clearRelation() 0 7 1
A getDispatcher() 0 9 3
A listen() 0 4 1
A saving() 0 5 1
A saved() 0 5 1
A creating() 0 4 1
A created() 0 4 1
A updating() 0 4 1
A updated() 0 4 1
A deleting() 0 4 1
A deleted() 0 4 1
A performDispatch() 0 16 3
A getErrors() 0 8 2
A valid() 0 21 4
B filterAndValidate() 0 20 8
A validateValue() 0 24 4
A checkUniqueness() 0 15 2
A getPropertyTitle() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

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