Completed
Push — master ( 4174bd...a2828a )
by Jared
02:06
created

Model::clearCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use BadMethodCallException;
15
use ICanBoogie\Inflector;
16
use Pulsar\Driver\DriverInterface;
17
use Pulsar\Exception\DriverMissingException;
18
use Pulsar\Exception\MassAssignmentException;
19
use Pulsar\Exception\ModelException;
20
use Pulsar\Exception\ModelNotFoundException;
21
use Pulsar\Relation\BelongsTo;
22
use Pulsar\Relation\BelongsToMany;
23
use Pulsar\Relation\HasMany;
24
use Pulsar\Relation\HasOne;
25
use Pulsar\Relation\Relation;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
28
/**
29
 * Class Model.
30
 *
31
 * @method Query             where($where, $value = null, $condition = null)
32
 * @method Query             limit($limit)
33
 * @method Query             start($start)
34
 * @method Query             sort($sort)
35
 * @method Query             join($model, $column, $foreignKey)
36
 * @method Query             with($k)
37
 * @method Iterator          all()
38
 * @method array|static|null first($limit = 1)
39
 * @method int               count()
40
 * @method number            sum($property)
41
 * @method number            average($property)
42
 * @method number            max($property)
43
 * @method number            min($property)
44
 */
45
abstract class Model implements \ArrayAccess
46
{
47
    const IMMUTABLE = 0;
48
    const MUTABLE_CREATE_ONLY = 1;
49
    const MUTABLE = 2;
50
51
    const TYPE_STRING = 'string';
52
    const TYPE_NUMBER = 'number'; // DEPRECATED
53
    const TYPE_INTEGER = 'integer';
54
    const TYPE_FLOAT = 'float';
55
    const TYPE_BOOLEAN = 'boolean';
56
    const TYPE_DATE = 'date';
57
    const TYPE_OBJECT = 'object';
58
    const TYPE_ARRAY = 'array';
59
60
    const RELATIONSHIP_HAS_ONE = 'has_one';
61
    const RELATIONSHIP_HAS_MANY = 'has_many';
62
    const RELATIONSHIP_BELONGS_TO = 'belongs_to';
63
    const RELATIONSHIP_BELONGS_TO_MANY = 'belongs_to_many';
64
65
    const DEFAULT_ID_PROPERTY = 'id';
66
67
    /////////////////////////////
68
    // Model visible variables
69
    /////////////////////////////
70
71
    /**
72
     * List of model ID property names.
73
     *
74
     * @var array
75
     */
76
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
77
78
    /**
79
     * Property definitions expressed as a key-value map with
80
     * property names as the keys.
81
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
82
     *
83
     * @var array
84
     */
85
    protected static $properties = [];
86
87
    /**
88
     * @var array
89
     */
90
    protected static $dispatchers;
91
92
    /**
93
     * @var number|string|false
94
     */
95
    protected $_id;
96
97
    /**
98
     * @var array
99
     */
100
    protected $_ids;
101
102
    /**
103
     * @var array
104
     */
105
    protected $_values = [];
106
107
    /**
108
     * @var array
109
     */
110
    protected $_unsaved = [];
111
112
    /**
113
     * @var bool
114
     */
115
    protected $_persisted = false;
116
117
    /**
118
     * @var bool
119
     */
120
    protected $_loaded = false;
121
122
    /**
123
     * @var array
124
     */
125
    protected $_relationships = [];
126
127
    /**
128
     * @var Errors
129
     */
130
    protected $_errors;
131
132
    /////////////////////////////
133
    // Base model variables
134
    /////////////////////////////
135
136
    /**
137
     * @var array
138
     */
139
    private static $propertyDefinitionBase = [
140
        'type' => null,
141
        'mutable' => self::MUTABLE,
142
        'null' => false,
143
        'unique' => false,
144
        'required' => false,
145
    ];
146
147
    /**
148
     * @var array
149
     */
150
    private static $defaultIDProperty = [
151
        'type' => self::TYPE_INTEGER,
152
        'mutable' => self::IMMUTABLE,
153
    ];
154
155
    /**
156
     * @var array
157
     */
158
    private static $timestampProperties = [
159
        'created_at' => [
160
            'type' => self::TYPE_DATE,
161
            'validate' => 'timestamp|db_timestamp',
162
        ],
163
        'updated_at' => [
164
            'type' => self::TYPE_DATE,
165
            'validate' => 'timestamp|db_timestamp',
166
        ],
167
    ];
168
169
    /**
170
     * @var array
171
     */
172
    private static $softDeleteProperties = [
173
        'deleted_at' => [
174
            'type' => self::TYPE_DATE,
175
            'validate' => 'timestamp|db_timestamp',
176
            'null' => true,
177
        ],
178
    ];
179
180
    /**
181
     * @var array
182
     */
183
    private static $initialized = [];
184
185
    /**
186
     * @var DriverInterface
187
     */
188
    private static $driver;
189
190
    /**
191
     * @var array
192
     */
193
    private static $accessors = [];
194
195
    /**
196
     * @var array
197
     */
198
    private static $mutators = [];
199
200
    /**
201
     * @var bool
202
     */
203
    private $_ignoreUnsaved;
204
205
    /**
206
     * Creates a new model object.
207
     *
208
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
209
     * @param array                    $values optional key-value map to pre-seed model
210
     */
211
    public function __construct($id = false, array $values = [])
212
    {
213
        // initialize the model
214
        $this->init();
215
216
        // parse the supplied model ID
217
        $this->parseId($id);
218
219
        // load any given values
220
        if (count($values) > 0) {
221
            $this->refreshWith($values);
222
        }
223
    }
224
225
    /**
226
     * Performs initialization on this model.
227
     */
228
    private function init()
229
    {
230
        // ensure the initialize function is called only once
231
        $k = get_called_class();
232
        if (!isset(self::$initialized[$k])) {
233
            $this->initialize();
234
            self::$initialized[$k] = true;
235
        }
236
    }
237
238
    /**
239
     * The initialize() method is called once per model. It's used
240
     * to perform any one-off tasks before the model gets
241
     * constructed. This is a great place to add any model
242
     * properties. When extending this method be sure to call
243
     * parent::initialize() as some important stuff happens here.
244
     * If extending this method to add properties then you should
245
     * call parent::initialize() after adding any properties.
246
     */
247
    protected function initialize()
248
    {
249
        // load the driver
250
        static::getDriver();
251
252
        // add in the default ID property
253
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
254
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
255
        }
256
257
        // generates created_at and updated_at timestamps
258
        if (property_exists($this, 'autoTimestamps')) {
259
            $this->installAutoTimestamps();
260
        }
261
262
        // generates deleted_at timestamps
263
        if (property_exists($this, 'softDelete')) {
264
            $this->installSoftDelete();
265
        }
266
267
        // fill in each property by extending the property
268
        // definition base
269
        foreach (static::$properties as $k => &$property) {
270
            $property = array_replace(self::$propertyDefinitionBase, $property);
271
272
            // populate relationship property settings
273
            if (isset($property['relation'])) {
274
                // this is added for BC with older versions of pulsar
275
                // that only supported belongs to relationships
276
                if (!isset($property['relation_type'])) {
277
                    $property['relation_type'] = self::RELATIONSHIP_BELONGS_TO;
278
                    $property['local_key'] = $k;
279
                }
280
281
                $relation = $this->getRelationshipManager($k);
0 ignored issues
show
Documentation introduced by
$k is of type integer|string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282
                if (!isset($property['foreign_key'])) {
283
                    $property['foreign_key'] = $relation->getForeignKey();
284
                }
285
286
                if (!isset($property['local_key'])) {
287
                    $property['local_key'] = $relation->getLocalKey();
288
                }
289
290
                if (!isset($property['pivot_tablename']) && $relation instanceof BelongsToMany) {
291
                    $property['pivot_tablename'] = $relation->getTablename();
292
                }
293
            }
294
        }
295
296
        // order the properties array by name for consistency
297
        // since it is constructed in a random order
298
        ksort(static::$properties);
299
    }
300
301
    /**
302
     * Installs the `created_at` and `updated_at` properties.
303
     */
304
    private function installAutoTimestamps()
305
    {
306
        static::$properties = array_replace(self::$timestampProperties, static::$properties);
307
308
        self::creating(function (ModelEvent $event) {
309
            $model = $event->getModel();
310
            $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...
311
            $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...
312
        });
313
314
        self::updating(function (ModelEvent $event) {
315
            $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...
316
        });
317
    }
318
319
    /**
320
     * Installs the `deleted_at` properties.
321
     */
322
    private function installSoftDelete()
323
    {
324
        static::$properties = array_replace(self::$softDeleteProperties, static::$properties);
325
    }
326
327
    /**
328
     * Parses the given ID, which can be a single or composite primary key.
329
     *
330
     * @param mixed $id
331
     */
332
    private function parseId($id)
333
    {
334
        if (is_array($id)) {
335
            // A model can be supplied as a primary key
336
            foreach ($id as &$el) {
337
                if ($el instanceof self) {
338
                    $el = $el->id();
339
                }
340
            }
341
342
            // The IDs come in as the same order as ::$ids.
343
            // We need to match up the elements on that
344
            // input into a key-value map for each ID property.
345
            $ids = [];
346
            $idQueue = array_reverse($id);
347
            foreach (static::$ids as $k => $f) {
348
                // type cast
349
                if (count($idQueue) > 0) {
350
                    $idProperty = static::getProperty($f);
351
                    $ids[$f] = static::cast($idProperty, array_pop($idQueue));
0 ignored issues
show
Bug introduced by
It seems like $idProperty defined by static::getProperty($f) on line 350 can also be of type null; however, Pulsar\Model::cast() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
352
                } else {
353
                    $ids[$f] = false;
354
                }
355
            }
356
357
            $this->_id = implode(',', $id);
358
            $this->_ids = $ids;
359
        } elseif ($id instanceof self) {
360
            // A model can be supplied as a primary key
361
            $this->_id = $id->id();
362
            $this->_ids = $id->ids();
363
        } else {
364
            // type cast the single primary key
365
            $idName = static::$ids[0];
366
            if (false !== $id) {
367
                $idProperty = static::getProperty($idName);
368
                $id = static::cast($idProperty, $id);
0 ignored issues
show
Bug introduced by
It seems like $idProperty defined by static::getProperty($idName) on line 367 can also be of type null; however, Pulsar\Model::cast() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
369
            }
370
371
            $this->_id = $id;
372
            $this->_ids = [$idName => $id];
373
        }
374
    }
375
376
    /**
377
     * Sets the driver for all models.
378
     *
379
     * @param DriverInterface $driver
380
     */
381
    public static function setDriver(DriverInterface $driver)
382
    {
383
        self::$driver = $driver;
384
    }
385
386
    /**
387
     * Gets the driver for all models.
388
     *
389
     * @return DriverInterface
390
     *
391
     * @throws DriverMissingException when a driver has not been set yet
392
     */
393
    public static function getDriver()
394
    {
395
        if (!self::$driver) {
396
            throw new DriverMissingException('A model driver has not been set yet.');
397
        }
398
399
        return self::$driver;
400
    }
401
402
    /**
403
     * Clears the driver for all models.
404
     */
405
    public static function clearDriver()
406
    {
407
        self::$driver = null;
408
    }
409
410
    /**
411
     * Gets the name of the model, i.e. User.
412
     *
413
     * @return string
414
     */
415
    public static function modelName()
416
    {
417
        // strip namespacing
418
        $paths = explode('\\', get_called_class());
419
420
        return end($paths);
421
    }
422
423
    /**
424
     * Gets the model ID.
425
     *
426
     * @return string|number|false ID
427
     */
428
    public function id()
429
    {
430
        return $this->_id;
431
    }
432
433
    /**
434
     * Gets a key-value map of the model ID.
435
     *
436
     * @return array ID map
437
     */
438
    public function ids()
439
    {
440
        return $this->_ids;
441
    }
442
443
    /////////////////////////////
444
    // Magic Methods
445
    /////////////////////////////
446
447
    /**
448
     * Converts the model into a string.
449
     *
450
     * @return string
451
     */
452
    public function __toString()
453
    {
454
        $values = array_merge($this->_values, $this->_unsaved, $this->_ids);
455
        ksort($values);
456
457
        return get_called_class().'('.json_encode($values, JSON_PRETTY_PRINT).')';
458
    }
459
460
    /**
461
     * Shortcut to a get() call for a given property.
462
     *
463
     * @param string $name
464
     *
465
     * @return mixed
466
     */
467
    public function __get($name)
468
    {
469
        $result = $this->get([$name]);
470
471
        return reset($result);
472
    }
473
474
    /**
475
     * Sets an unsaved value.
476
     *
477
     * @param string $name
478
     * @param mixed  $value
479
     */
480
    public function __set($name, $value)
481
    {
482
        // if changing property, remove relation model
483
        if (isset($this->_relationships[$name])) {
484
            unset($this->_relationships[$name]);
485
        }
486
487
        // call any mutators
488
        $mutator = self::getMutator($name);
489
        if ($mutator) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mutator of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
490
            $this->_unsaved[$name] = $this->$mutator($value);
491
        } else {
492
            $this->_unsaved[$name] = $value;
493
        }
494
    }
495
496
    /**
497
     * Checks if an unsaved value or property exists by this name.
498
     *
499
     * @param string $name
500
     *
501
     * @return bool
502
     */
503
    public function __isset($name)
504
    {
505
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
506
    }
507
508
    /**
509
     * Unsets an unsaved value.
510
     *
511
     * @param string $name
512
     */
513
    public function __unset($name)
514
    {
515
        if (array_key_exists($name, $this->_unsaved)) {
516
            // if changing property, remove relation model
517
            if (isset($this->_relationships[$name])) {
518
                unset($this->_relationships[$name]);
519
            }
520
521
            unset($this->_unsaved[$name]);
522
        }
523
    }
524
525
    /////////////////////////////
526
    // ArrayAccess Interface
527
    /////////////////////////////
528
529
    public function offsetExists($offset)
530
    {
531
        return isset($this->$offset);
532
    }
533
534
    public function offsetGet($offset)
535
    {
536
        return $this->$offset;
537
    }
538
539
    public function offsetSet($offset, $value)
540
    {
541
        $this->$offset = $value;
542
    }
543
544
    public function offsetUnset($offset)
545
    {
546
        unset($this->$offset);
547
    }
548
549
    public static function __callStatic($name, $parameters)
550
    {
551
        // Any calls to unkown static methods should be deferred to
552
        // the query. This allows calls like User::where()
553
        // to replace User::query()->where().
554
        return call_user_func_array([static::query(), $name], $parameters);
555
    }
556
557
    /////////////////////////////
558
    // Property Definitions
559
    /////////////////////////////
560
561
    /**
562
     * Gets all the property definitions for the model.
563
     *
564
     * @return array key-value map of properties
565
     */
566
    public static function getProperties()
567
    {
568
        return static::$properties;
569
    }
570
571
    /**
572
     * Gets a property defition for the model.
573
     *
574
     * @param string $property property to lookup
575
     *
576
     * @return array|null property
577
     */
578
    public static function getProperty($property)
579
    {
580
        return array_value(static::$properties, $property);
581
    }
582
583
    /**
584
     * Gets the names of the model ID properties.
585
     *
586
     * @return array
587
     */
588
    public static function getIDProperties()
589
    {
590
        return static::$ids;
591
    }
592
593
    /**
594
     * Checks if the model has a property.
595
     *
596
     * @param string $property property
597
     *
598
     * @return bool has property
599
     */
600
    public static function hasProperty($property)
601
    {
602
        return isset(static::$properties[$property]);
603
    }
604
605
    /**
606
     * Gets the mutator method name for a given proeprty name.
607
     * Looks for methods in the form of `setPropertyValue`.
608
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
609
     *
610
     * @param string $property property
611
     *
612
     * @return string|false method name if it exists
613
     */
614 View Code Duplication
    public static function getMutator($property)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
615
    {
616
        $class = get_called_class();
617
618
        $k = $class.':'.$property;
619
        if (!array_key_exists($k, self::$mutators)) {
620
            $inflector = Inflector::get();
621
            $method = 'set'.$inflector->camelize($property).'Value';
622
623
            if (!method_exists($class, $method)) {
624
                $method = false;
625
            }
626
627
            self::$mutators[$k] = $method;
628
        }
629
630
        return self::$mutators[$k];
631
    }
632
633
    /**
634
     * Gets the accessor method name for a given proeprty name.
635
     * Looks for methods in the form of `getPropertyValue`.
636
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
637
     *
638
     * @param string $property property
639
     *
640
     * @return string|false method name if it exists
641
     */
642 View Code Duplication
    public static function getAccessor($property)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
643
    {
644
        $class = get_called_class();
645
646
        $k = $class.':'.$property;
647
        if (!array_key_exists($k, self::$accessors)) {
648
            $inflector = Inflector::get();
649
            $method = 'get'.$inflector->camelize($property).'Value';
650
651
            if (!method_exists($class, $method)) {
652
                $method = false;
653
            }
654
655
            self::$accessors[$k] = $method;
656
        }
657
658
        return self::$accessors[$k];
659
    }
660
661
    /**
662
     * Marshals a value for a given property from storage.
663
     *
664
     * @param array $property
665
     * @param mixed $value
666
     *
667
     * @return mixed type-casted value
668
     */
669
    public static function cast(array $property, $value)
670
    {
671
        if (null === $value) {
672
            return;
673
        }
674
675
        // handle empty strings as null
676
        if ($property['null'] && '' == $value) {
677
            return;
678
        }
679
680
        $type = array_value($property, 'type');
681
        $m = 'to_'.$type;
682
683
        if (!method_exists(Property::class, $m)) {
684
            return $value;
685
        }
686
687
        return Property::$m($value);
688
    }
689
690
    /////////////////////////////
691
    // CRUD Operations
692
    /////////////////////////////
693
694
    /**
695
     * Gets the tablename for storing this model.
696
     *
697
     * @return string
698
     */
699
    public function getTablename()
700
    {
701
        $inflector = Inflector::get();
702
703
        return $inflector->camelize($inflector->pluralize(static::modelName()));
704
    }
705
706
    /**
707
     * Gets the ID of the connection in the connection manager
708
     * that stores this model.
709
     *
710
     * @return string|null
711
     */
712
    public function getConnection()
713
    {
714
        return null;
715
    }
716
717
    protected function usesTransactions(): bool
718
    {
719
        return false;
720
    }
721
722
    /**
723
     * Saves the model.
724
     *
725
     * @return bool true when the operation was successful
726
     */
727
    public function save()
728
    {
729
        if (false === $this->_id) {
730
            return $this->create();
731
        }
732
733
        return $this->set();
734
    }
735
736
    /**
737
     * Saves the model. Throws an exception when the operation fails.
738
     *
739
     * @throws ModelException when the model cannot be saved
740
     */
741
    public function saveOrFail()
742
    {
743
        if (!$this->save()) {
744
            $msg = 'Failed to save '.static::modelName();
745
            if ($validationErrors = $this->getErrors()->all()) {
746
                $msg .= ': '.implode(', ', $validationErrors);
747
            }
748
749
            throw new ModelException($msg);
750
        }
751
    }
752
753
    /**
754
     * Creates a new model.
755
     *
756
     * @param array $data optional key-value properties to set
757
     *
758
     * @return bool true when the operation was successful
759
     *
760
     * @throws BadMethodCallException when called on an existing model
761
     */
762
    public function create(array $data = [])
763
    {
764
        if (false !== $this->_id) {
765
            throw new BadMethodCallException('Cannot call create() on an existing model');
766
        }
767
768
        // mass assign values passed into create()
769
        $this->setValues($data);
770
771
        // clear any previous errors
772
        $this->getErrors()->clear();
773
774
        // start a DB transaction if needed
775
        $usesTransactions = $this->usesTransactions();
776
        if ($usesTransactions) {
777
            self::$driver->startTransaction($this->getConnection());
778
        }
779
780
        // dispatch the model.creating event
781
        if (!$this->performDispatch(ModelEvent::CREATING, $usesTransactions)) {
782
            return false;
783
        }
784
785
        $requiredProperties = [];
786
        foreach (static::$properties as $name => $property) {
787
            // build a list of the required properties
788
            if ($property['required']) {
789
                $requiredProperties[] = $name;
790
            }
791
792
            // add in default values
793
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
794
                $this->_unsaved[$name] = $property['default'];
795
            }
796
        }
797
798
        // validate the values being saved
799
        $validated = true;
800
        $insertArray = [];
801
        foreach ($this->_unsaved as $name => $value) {
802
            // exclude if value does not map to a property
803
            if (!isset(static::$properties[$name])) {
804
                continue;
805
            }
806
807
            $property = static::$properties[$name];
808
809
            // cannot insert immutable values
810
            // (unless using the default value)
811
            if (self::IMMUTABLE == $property['mutable'] && $value !== $this->getPropertyDefault($property)) {
812
                continue;
813
            }
814
815
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
816
            $insertArray[$name] = $value;
817
        }
818
819
        // check for required fields
820
        foreach ($requiredProperties as $name) {
821
            if (!isset($insertArray[$name])) {
822
                $property = static::$properties[$name];
823
                $params = [
824
                    'field' => $name,
825
                    'field_name' => $this->getPropertyTitle($property, $name),
826
                ];
827
                $this->getErrors()->add('pulsar.validation.required', $params);
828
829
                $validated = false;
830
            }
831
        }
832
833
        if (!$validated) {
834
            // when validations fail roll back any database transaction
835
            if ($usesTransactions) {
836
                self::$driver->rollBackTransaction($this->getConnection());
837
            }
838
839
            return false;
840
        }
841
842
        $created = self::$driver->createModel($this, $insertArray);
843
844 View Code Duplication
        if ($created) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
845
            // determine the model's new ID
846
            $this->getNewID();
847
848
            // store the persisted values to the in-memory cache
849
            $this->_unsaved = [];
850
            $this->refreshWith(array_replace($this->_ids, $insertArray));
851
852
            // dispatch the model.created event
853
            if (!$this->performDispatch(ModelEvent::CREATED, $usesTransactions)) {
854
                return false;
855
            }
856
        }
857
858
        // commit the transaction, if used
859
        if ($usesTransactions) {
860
            self::$driver->commitTransaction($this->getConnection());
861
        }
862
863
        return $created;
864
    }
865
866
    /**
867
     * Ignores unsaved values when fetching the next value.
868
     *
869
     * @return $this
870
     */
871
    public function ignoreUnsaved()
872
    {
873
        $this->_ignoreUnsaved = true;
874
875
        return $this;
876
    }
877
878
    /**
879
     * Fetches property values from the model.
880
     *
881
     * This method looks up values in this order:
882
     * IDs, local cache, unsaved values, storage layer, defaults
883
     *
884
     * @param array $properties list of property names to fetch values of
885
     *
886
     * @return array
887
     */
888
    public function get(array $properties)
889
    {
890
        // load the values from the IDs and local model cache
891
        $values = array_replace($this->ids(), $this->_values);
892
893
        // unless specified, use any unsaved values
894
        $ignoreUnsaved = $this->_ignoreUnsaved;
895
        $this->_ignoreUnsaved = false;
896
897
        if (!$ignoreUnsaved) {
898
            $values = array_replace($values, $this->_unsaved);
899
        }
900
901
        // see if there are any model properties that do not exist.
902
        // when true then this means the model needs to be hydrated
903
        // NOTE: only looking at model properties and excluding dynamic/non-existent properties
904
        $modelProperties = array_keys(static::$properties);
905
        $numMissing = count(array_intersect($modelProperties, array_diff($properties, array_keys($values))));
906
907
        if ($numMissing > 0 && !$this->_loaded) {
908
            // load the model from the storage layer, if needed
909
            $this->refresh();
910
911
            $values = array_replace($values, $this->_values);
912
913
            if (!$ignoreUnsaved) {
914
                $values = array_replace($values, $this->_unsaved);
915
            }
916
        }
917
918
        // build a key-value map of the requested properties
919
        $return = [];
920
        foreach ($properties as $k) {
921
            $return[$k] = $this->getValue($k, $values);
922
        }
923
924
        return $return;
925
    }
926
927
    /**
928
     * Gets a property value from the model.
929
     *
930
     * Values are looked up in this order:
931
     *  1. unsaved values
932
     *  2. local values
933
     *  3. default value
934
     *  4. null
935
     *
936
     * @param string $property
937
     * @param array  $values
938
     *
939
     * @return mixed
940
     */
941
    protected function getValue($property, array $values)
942
    {
943
        $value = null;
944
945
        if (array_key_exists($property, $values)) {
946
            $value = $values[$property];
947
        } elseif (static::hasProperty($property)) {
948
            $value = $this->_values[$property] = $this->getPropertyDefault(static::$properties[$property]);
949
        }
950
951
        // call any accessors
952
        if ($accessor = self::getAccessor($property)) {
953
            $value = $this->$accessor($value);
954
        }
955
956
        return $value;
957
    }
958
959
    /**
960
     * Populates a newly created model with its ID.
961
     */
962
    protected function getNewID()
963
    {
964
        $ids = [];
965
        $namedIds = [];
966
        foreach (static::$ids as $k) {
967
            // attempt use the supplied value if the ID property is mutable
968
            $property = static::getProperty($k);
969
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
970
                $id = $this->_unsaved[$k];
971
            } else {
972
                $id = self::$driver->getCreatedID($this, $k);
973
            }
974
975
            $ids[] = $id;
976
            $namedIds[$k] = $id;
977
        }
978
979
        $this->_id = implode(',', $ids);
980
        $this->_ids = $namedIds;
981
    }
982
983
    /**
984
     * Sets a collection values on the model from an untrusted input.
985
     *
986
     * @param array $values
987
     *
988
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
989
     *
990
     * @return $this
991
     */
992
    public function setValues($values)
993
    {
994
        // check if the model has a mass assignment whitelist
995
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
996
997
        // if no whitelist, then check for a blacklist
998
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
999
1000
        foreach ($values as $k => $value) {
1001
            // check for mass assignment violations
1002
            if (($permitted && !in_array($k, $permitted)) ||
1003
                ($protected && in_array($k, $protected))) {
1004
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
1005
            }
1006
1007
            $this->$k = $value;
1008
        }
1009
1010
        return $this;
1011
    }
1012
1013
    /**
1014
     * Converts the model to an array.
1015
     *
1016
     * @return array
1017
     */
1018
    public function toArray()
1019
    {
1020
        // build the list of properties to retrieve
1021
        $properties = array_keys(static::$properties);
1022
1023
        // remove any hidden properties
1024
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
1025
        $properties = array_diff($properties, $hide);
1026
1027
        // add any appended properties
1028
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
1029
        $properties = array_merge($properties, $append);
1030
1031
        // get the values for the properties
1032
        $result = $this->get($properties);
1033
1034
        foreach ($result as $k => &$value) {
1035
            // convert any models to arrays
1036
            if ($value instanceof self) {
1037
                $value = $value->toArray();
1038
            }
1039
        }
1040
1041
        // DEPRECATED
1042
        // apply the transformation hook
1043
        if (method_exists($this, 'toArrayHook')) {
1044
            $this->toArrayHook($result, [], [], []);
0 ignored issues
show
Bug introduced by
The method toArrayHook() does not exist on Pulsar\Model. Did you maybe mean toArray()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1045
        }
1046
1047
        return $result;
1048
    }
1049
1050
    /**
1051
     * Updates the model.
1052
     *
1053
     * @param array $data optional key-value properties to set
1054
     *
1055
     * @return bool true when the operation was successful
1056
     *
1057
     * @throws BadMethodCallException when not called on an existing model
1058
     */
1059
    public function set(array $data = [])
1060
    {
1061
        if (false === $this->_id) {
1062
            throw new BadMethodCallException('Can only call set() on an existing model');
1063
        }
1064
1065
        // mass assign values passed into set()
1066
        $this->setValues($data);
1067
1068
        // clear any previous errors
1069
        $this->getErrors()->clear();
1070
1071
        // not updating anything?
1072
        if (0 == count($this->_unsaved)) {
1073
            return true;
1074
        }
1075
1076
        // start a DB transaction if needed
1077
        $usesTransactions = $this->usesTransactions();
1078
        if ($usesTransactions) {
1079
            self::$driver->startTransaction($this->getConnection());
1080
        }
1081
1082
        // dispatch the model.updating event
1083
        if (!$this->performDispatch(ModelEvent::UPDATING, $usesTransactions)) {
1084
            return false;
1085
        }
1086
1087
        // validate the values being saved
1088
        $validated = true;
1089
        $updateArray = [];
1090
        foreach ($this->_unsaved as $name => $value) {
1091
            // exclude if value does not map to a property
1092
            if (!isset(static::$properties[$name])) {
1093
                continue;
1094
            }
1095
1096
            $property = static::$properties[$name];
1097
1098
            // can only modify mutable properties
1099
            if (self::MUTABLE != $property['mutable']) {
1100
                continue;
1101
            }
1102
1103
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
1104
            $updateArray[$name] = $value;
1105
        }
1106
1107
        if (!$validated) {
1108
            // when validations fail roll back any database transaction
1109
            if ($usesTransactions) {
1110
                self::$driver->rollBackTransaction($this->getConnection());
1111
            }
1112
1113
            return false;
1114
        }
1115
1116
        $updated = self::$driver->updateModel($this, $updateArray);
1117
1118 View Code Duplication
        if ($updated) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1119
            // store the persisted values to the in-memory cache
1120
            $this->_unsaved = [];
1121
            $this->refreshWith(array_replace($this->_values, $updateArray));
1122
1123
            // dispatch the model.updated event
1124
            if (!$this->performDispatch(ModelEvent::UPDATED, $usesTransactions)) {
1125
                return false;
1126
            }
1127
        }
1128
1129
        // commit the transaction, if used
1130
        if ($usesTransactions) {
1131
            self::$driver->commitTransaction($this->getConnection());
1132
        }
1133
1134
        return $updated;
1135
    }
1136
1137
    /**
1138
     * Delete the model.
1139
     *
1140
     * @return bool true when the operation was successful
1141
     */
1142
    public function delete()
1143
    {
1144
        if (false === $this->_id) {
1145
            throw new BadMethodCallException('Can only call delete() on an existing model');
1146
        }
1147
1148
        // clear any previous errors
1149
        $this->getErrors()->clear();
1150
1151
        // start a DB transaction if needed
1152
        $usesTransactions = $this->usesTransactions();
1153
        if ($usesTransactions) {
1154
            self::$driver->startTransaction($this->getConnection());
1155
        }
1156
1157
        // dispatch the model.deleting event
1158
        if (!$this->performDispatch(ModelEvent::DELETING, $usesTransactions)) {
1159
            return false;
1160
        }
1161
1162
        // perform a hard (default) or soft delete
1163
        $hardDelete = true;
1164
        if (property_exists($this, 'softDelete')) {
1165
            $t = time();
1166
            $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...
1167
            $t = $this->filterAndValidate(static::getProperty('deleted_at'), 'deleted_at', $t);
0 ignored issues
show
Bug introduced by
It seems like static::getProperty('deleted_at') targeting Pulsar\Model::getProperty() can also be of type null; however, Pulsar\Model::filterAndValidate() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1168
            $deleted = self::$driver->updateModel($this, ['deleted_at' => $t]);
1169
            $hardDelete = false;
1170
        } else {
1171
            $deleted = self::$driver->deleteModel($this);
1172
        }
1173
1174
        if ($deleted) {
1175
            // dispatch the model.deleted event
1176
            if (!$this->performDispatch(ModelEvent::DELETED, $usesTransactions)) {
1177
                return false;
1178
            }
1179
1180
            if ($hardDelete) {
1181
                $this->_persisted = false;
1182
            }
1183
        }
1184
1185
        // commit the transaction, if used
1186
        if ($usesTransactions) {
1187
            self::$driver->commitTransaction($this->getConnection());
1188
        }
1189
1190
        return $deleted;
1191
    }
1192
1193
    /**
1194
     * Restores a soft-deleted model.
1195
     *
1196
     * @return bool
1197
     */
1198
    public function restore()
1199
    {
1200
        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...
1201
            throw new BadMethodCallException('Can only call restore() on a soft-deleted model');
1202
        }
1203
1204
        // start a DB transaction if needed
1205
        $usesTransactions = $this->usesTransactions();
1206
        if ($usesTransactions) {
1207
            self::$driver->startTransaction($this->getConnection());
1208
        }
1209
1210
        // dispatch the model.updating event
1211
        if (!$this->performDispatch(ModelEvent::UPDATING, $usesTransactions)) {
1212
            return false;
1213
        }
1214
1215
        $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...
1216
        $restored = self::$driver->updateModel($this, ['deleted_at' => null]);
1217
1218
        if ($restored) {
1219
            // dispatch the model.updated event
1220
            if (!$this->performDispatch(ModelEvent::UPDATED, $usesTransactions)) {
1221
                return false;
1222
            }
1223
        }
1224
1225
        // commit the transaction, if used
1226
        if ($usesTransactions) {
1227
            self::$driver->commitTransaction($this->getConnection());
1228
        }
1229
1230
        return $restored;
1231
    }
1232
1233
    /**
1234
     * Checks if the model has been deleted.
1235
     *
1236
     * @return bool
1237
     */
1238
    public function isDeleted()
1239
    {
1240
        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...
1241
            return true;
1242
        }
1243
1244
        return !$this->_persisted;
1245
    }
1246
1247
    /////////////////////////////
1248
    // Queries
1249
    /////////////////////////////
1250
1251
    /**
1252
     * Generates a new query instance.
1253
     *
1254
     * @return Query
1255
     */
1256
    public static function query()
1257
    {
1258
        // Create a new model instance for the query to ensure
1259
        // that the model's initialize() method gets called.
1260
        // Otherwise, the property definitions will be incomplete.
1261
        $model = new static();
1262
        $query = new Query($model);
1263
1264
        // scope soft-deleted models to only include non-deleted models
1265
        if (property_exists($model, 'softDelete')) {
1266
            $query->where('deleted_at IS NOT NULL');
1267
        }
1268
1269
        return $query;
1270
    }
1271
1272
    /**
1273
     * Generates a new query instance that includes soft-deleted models.
1274
     *
1275
     * @return Query
1276
     */
1277
    public static function withDeleted()
1278
    {
1279
        // Create a new model instance for the query to ensure
1280
        // that the model's initialize() method gets called.
1281
        // Otherwise, the property definitions will be incomplete.
1282
        $model = new static();
1283
1284
        return new Query($model);
1285
    }
1286
1287
    /**
1288
     * Finds a single instance of a model given it's ID.
1289
     *
1290
     * @param mixed $id
1291
     *
1292
     * @return static|null
1293
     */
1294
    public static function find($id)
1295
    {
1296
        $ids = [];
1297
        $id = (array) $id;
1298
        foreach (static::$ids as $j => $k) {
1299
            if ($_id = array_value($id, $j)) {
1300
                $ids[$k] = $_id;
1301
            }
1302
        }
1303
1304
        // malformed ID
1305
        if (count($ids) < count(static::$ids)) {
1306
            return null;
1307
        }
1308
1309
        return static::query()->where($ids)->first();
1310
    }
1311
1312
    /**
1313
     * Finds a single instance of a model given it's ID or throws an exception.
1314
     *
1315
     * @param mixed $id
1316
     *
1317
     * @return static
1318
     *
1319
     * @throws ModelNotFoundException when a model could not be found
1320
     */
1321
    public static function findOrFail($id)
1322
    {
1323
        $model = static::find($id);
0 ignored issues
show
Bug Compatibility introduced by
The expression static::find($id); of type null|array|Pulsar\Model adds the type array to the return on line 1328 which is incompatible with the return type documented by Pulsar\Model::findOrFail of type Pulsar\Model.
Loading history...
1324
        if (!$model) {
1325
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1326
        }
1327
1328
        return $model;
1329
    }
1330
1331
    /**
1332
     * @deprecated
1333
     *
1334
     * Checks if the model exists in the database
1335
     *
1336
     * @return bool
1337
     */
1338
    public function exists()
1339
    {
1340
        return 1 == static::query()->where($this->ids())->count();
1341
    }
1342
1343
    /**
1344
     * Tells if this model instance has been persisted to the data layer.
1345
     *
1346
     * NOTE: this does not actually perform a check with the data layer
1347
     *
1348
     * @return bool
1349
     */
1350
    public function persisted()
1351
    {
1352
        return $this->_persisted;
1353
    }
1354
1355
    /**
1356
     * Loads the model from the storage layer.
1357
     *
1358
     * @return $this
1359
     */
1360
    public function refresh()
1361
    {
1362
        if (false === $this->_id) {
1363
            return $this;
1364
        }
1365
1366
        $values = self::$driver->loadModel($this);
1367
1368
        if (!is_array($values)) {
1369
            return $this;
1370
        }
1371
1372
        // clear any relations
1373
        $this->_relationships = [];
1374
1375
        return $this->refreshWith($values);
1376
    }
1377
1378
    /**
1379
     * Loads values into the model.
1380
     *
1381
     * @param array $values values
1382
     *
1383
     * @return $this
1384
     */
1385
    public function refreshWith(array $values)
1386
    {
1387
        // type cast the values
1388
        foreach ($values as $k => &$value) {
1389
            if ($property = static::getProperty($k)) {
1390
                $value = static::cast($property, $value);
1391
            }
1392
        }
1393
1394
        $this->_loaded = true;
1395
        $this->_persisted = true;
1396
        $this->_values = $values;
1397
1398
        return $this;
1399
    }
1400
1401
    /**
1402
     * Clears the cache for this model.
1403
     *
1404
     * @return $this
1405
     */
1406
    public function clearCache()
1407
    {
1408
        $this->_loaded = false;
1409
        $this->_unsaved = [];
1410
        $this->_values = [];
1411
        $this->_relationships = [];
1412
1413
        return $this;
1414
    }
1415
1416
    /////////////////////////////
1417
    // Relationships
1418
    /////////////////////////////
1419
1420
    /**
1421
     * @deprecated
1422
     *
1423
     * Gets the model(s) for a relationship
1424
     *
1425
     * @param string $k property
1426
     *
1427
     * @throws \InvalidArgumentException when the relationship manager cannot be created
1428
     *
1429
     * @return Model|null
1430
     */
1431
    public function relation($k)
1432
    {
1433
        if (!array_key_exists($k, $this->_relationships)) {
1434
            $relation = $this->getRelationshipManager($k);
0 ignored issues
show
Documentation introduced by
$k is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1435
            $this->_relationships[$k] = $relation->getResults();
1436
        }
1437
1438
        return $this->_relationships[$k];
1439
    }
1440
1441
    /**
1442
     * @deprecated
1443
     *
1444
     * Sets the model for a one-to-one relationship (has-one or belongs-to)
1445
     *
1446
     * @param string $k
1447
     * @param Model  $model
1448
     *
1449
     * @return $this
1450
     */
1451
    public function setRelation($k, self $model)
1452
    {
1453
        $this->$k = $model->id();
1454
        $this->_relationships[$k] = $model;
1455
1456
        return $this;
1457
    }
1458
1459
    /**
1460
     * @deprecated
1461
     *
1462
     * Sets the model for a one-to-many relationship
1463
     *
1464
     * @param string   $k
1465
     * @param iterable $models
1466
     *
1467
     * @return $this
1468
     */
1469
    public function setRelationCollection($k, $models)
1470
    {
1471
        $this->_relationships[$k] = $models;
1472
1473
        return $this;
1474
    }
1475
1476
    /**
1477
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null.
1478
     *
1479
     * @param string $k
1480
     *
1481
     * @return $this
1482
     */
1483
    public function clearRelation($k)
1484
    {
1485
        $this->$k = null;
1486
        $this->_relationships[$k] = null;
1487
1488
        return $this;
1489
    }
1490
1491
    /**
1492
     * Builds a relationship manager object for a given property.
1493
     *
1494
     * @param array $k
1495
     *
1496
     * @throws \InvalidArgumentException when the relationship manager cannot be created
1497
     *
1498
     * @return Relation
1499
     */
1500
    public function getRelationshipManager($k)
1501
    {
1502
        $property = static::getProperty($k);
0 ignored issues
show
Documentation introduced by
$k is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1503
        if (!isset($property['relation'])) {
1504
            throw new \InvalidArgumentException('Property "'.$k.'" does not have a relationship.');
1505
        }
1506
1507
        $relationModelClass = $property['relation'];
1508
        $foreignKey = array_value($property, 'foreign_key');
1509
        $localKey = array_value($property, 'local_key');
1510
1511
        if (self::RELATIONSHIP_HAS_ONE == $property['relation_type']) {
1512
            return $this->hasOne($relationModelClass, $foreignKey, $localKey);
1513
        }
1514
1515
        if (self::RELATIONSHIP_HAS_MANY == $property['relation_type']) {
1516
            return $this->hasMany($relationModelClass, $foreignKey, $localKey);
1517
        }
1518
1519
        if (self::RELATIONSHIP_BELONGS_TO == $property['relation_type']) {
1520
            return $this->belongsTo($relationModelClass, $foreignKey, $localKey);
1521
        }
1522
1523
        if (self::RELATIONSHIP_BELONGS_TO_MANY == $property['relation_type']) {
1524
            $pivotTable = array_value($property, 'pivot_tablename');
1525
1526
            return $this->belongsToMany($relationModelClass, $pivotTable, $foreignKey, $localKey);
1527
        }
1528
1529
        throw new \InvalidArgumentException('Relationship type on "'.$k.'" property not supported: '.$property['relation_type']);
1530
    }
1531
1532
    /**
1533
     * Creates the parent side of a One-To-One relationship.
1534
     *
1535
     * @param string $model      foreign model class
1536
     * @param string $foreignKey identifying key on foreign model
1537
     * @param string $localKey   identifying key on local model
1538
     *
1539
     * @return HasOne
1540
     */
1541
    public function hasOne($model, $foreignKey = '', $localKey = '')
1542
    {
1543
        return new HasOne($this, $localKey, $model, $foreignKey);
1544
    }
1545
1546
    /**
1547
     * Creates the child side of a One-To-One or One-To-Many relationship.
1548
     *
1549
     * @param string $model      foreign model class
1550
     * @param string $foreignKey identifying key on foreign model
1551
     * @param string $localKey   identifying key on local model
1552
     *
1553
     * @return BelongsTo
1554
     */
1555
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1556
    {
1557
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1558
    }
1559
1560
    /**
1561
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1562
     *
1563
     * @param string $model      foreign model class
1564
     * @param string $foreignKey identifying key on foreign model
1565
     * @param string $localKey   identifying key on local model
1566
     *
1567
     * @return HasMany
1568
     */
1569
    public function hasMany($model, $foreignKey = '', $localKey = '')
1570
    {
1571
        return new HasMany($this, $localKey, $model, $foreignKey);
1572
    }
1573
1574
    /**
1575
     * Creates the child side of a Many-To-Many relationship.
1576
     *
1577
     * @param string $model      foreign model class
1578
     * @param string $tablename  pivot table name
1579
     * @param string $foreignKey identifying key on foreign model
1580
     * @param string $localKey   identifying key on local model
1581
     *
1582
     * @return BelongsToMany
1583
     */
1584
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1585
    {
1586
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1587
    }
1588
1589
    /////////////////////////////
1590
    // Events
1591
    /////////////////////////////
1592
1593
    /**
1594
     * Gets the event dispatcher.
1595
     *
1596
     * @return EventDispatcher
1597
     */
1598
    public static function getDispatcher($ignoreCache = false)
1599
    {
1600
        $class = get_called_class();
1601
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1602
            self::$dispatchers[$class] = new EventDispatcher();
1603
        }
1604
1605
        return self::$dispatchers[$class];
1606
    }
1607
1608
    /**
1609
     * Subscribes to a listener to an event.
1610
     *
1611
     * @param string   $event    event name
1612
     * @param callable $listener
1613
     * @param int      $priority optional priority, higher #s get called first
1614
     */
1615
    public static function listen($event, callable $listener, $priority = 0)
1616
    {
1617
        static::getDispatcher()->addListener($event, $listener, $priority);
1618
    }
1619
1620
    /**
1621
     * Adds a listener to the model.creating and model.updating events.
1622
     *
1623
     * @param callable $listener
1624
     * @param int      $priority
1625
     */
1626
    public static function saving(callable $listener, $priority = 0)
1627
    {
1628
        static::listen(ModelEvent::CREATING, $listener, $priority);
1629
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1630
    }
1631
1632
    /**
1633
     * Adds a listener to the model.created and model.updated events.
1634
     *
1635
     * @param callable $listener
1636
     * @param int      $priority
1637
     */
1638
    public static function saved(callable $listener, $priority = 0)
1639
    {
1640
        static::listen(ModelEvent::CREATED, $listener, $priority);
1641
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1642
    }
1643
1644
    /**
1645
     * Adds a listener to the model.creating event.
1646
     *
1647
     * @param callable $listener
1648
     * @param int      $priority
1649
     */
1650
    public static function creating(callable $listener, $priority = 0)
1651
    {
1652
        static::listen(ModelEvent::CREATING, $listener, $priority);
1653
    }
1654
1655
    /**
1656
     * Adds a listener to the model.created event.
1657
     *
1658
     * @param callable $listener
1659
     * @param int      $priority
1660
     */
1661
    public static function created(callable $listener, $priority = 0)
1662
    {
1663
        static::listen(ModelEvent::CREATED, $listener, $priority);
1664
    }
1665
1666
    /**
1667
     * Adds a listener to the model.updating event.
1668
     *
1669
     * @param callable $listener
1670
     * @param int      $priority
1671
     */
1672
    public static function updating(callable $listener, $priority = 0)
1673
    {
1674
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1675
    }
1676
1677
    /**
1678
     * Adds a listener to the model.updated event.
1679
     *
1680
     * @param callable $listener
1681
     * @param int      $priority
1682
     */
1683
    public static function updated(callable $listener, $priority = 0)
1684
    {
1685
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1686
    }
1687
1688
    /**
1689
     * Adds a listener to the model.deleting event.
1690
     *
1691
     * @param callable $listener
1692
     * @param int      $priority
1693
     */
1694
    public static function deleting(callable $listener, $priority = 0)
1695
    {
1696
        static::listen(ModelEvent::DELETING, $listener, $priority);
1697
    }
1698
1699
    /**
1700
     * Adds a listener to the model.deleted event.
1701
     *
1702
     * @param callable $listener
1703
     * @param int      $priority
1704
     */
1705
    public static function deleted(callable $listener, $priority = 0)
1706
    {
1707
        static::listen(ModelEvent::DELETED, $listener, $priority);
1708
    }
1709
1710
    /**
1711
     * Dispatches the given event and checks if it was successful.
1712
     *
1713
     * @return bool true if the events were successfully propagated
1714
     */
1715
    private function performDispatch(string $eventName, bool $usesTransactions): bool
1716
    {
1717
        $event = new ModelEvent($this);
1718
        static::getDispatcher()->dispatch($event, $eventName);
1719
1720
        // when listeners fail roll back any database transaction
1721
        if ($event->isPropagationStopped()) {
1722
            if ($usesTransactions) {
1723
                self::$driver->rollBackTransaction($this->getConnection());
1724
            }
1725
1726
            return false;
1727
        }
1728
1729
        // DEPRECATED
1730
        if (ModelEvent::UPDATING == $eventName && !$event->isPropagationStopped() && method_exists($this, 'preSetHook')) {
1731
            if (!$this->preSetHook($this->_unsaved)) {
0 ignored issues
show
Bug introduced by
The method preSetHook() does not seem to exist on object<Pulsar\Model>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1732
                // when listeners fail roll back any database transaction
1733
                if ($usesTransactions) {
1734
                    self::$driver->rollBackTransaction($this->getConnection());
1735
                }
1736
1737
                return false;
1738
            }
1739
        }
1740
1741
        return true;
1742
    }
1743
1744
    /////////////////////////////
1745
    // Validation
1746
    /////////////////////////////
1747
1748
    /**
1749
     * Gets the error stack for this model.
1750
     *
1751
     * @return Errors
1752
     */
1753
    public function getErrors()
1754
    {
1755
        if (!$this->_errors) {
1756
            $this->_errors = new Errors();
1757
        }
1758
1759
        return $this->_errors;
1760
    }
1761
1762
    /**
1763
     * Checks if the model in its current state is valid.
1764
     *
1765
     * @return bool
1766
     */
1767
    public function valid()
1768
    {
1769
        // clear any previous errors
1770
        $this->getErrors()->clear();
1771
1772
        // run the validator against the model values
1773
        $values = $this->_unsaved + $this->_values;
1774
1775
        $validated = true;
1776
        foreach ($values as $k => $v) {
1777
            $property = static::getProperty($k);
1778
            $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 1777 can also be of type null; however, Pulsar\Model::filterAndValidate() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1779
        }
1780
1781
        // add back any modified unsaved values
1782
        foreach (array_keys($this->_unsaved) as $k) {
1783
            $this->_unsaved[$k] = $values[$k];
1784
        }
1785
1786
        return $validated;
1787
    }
1788
1789
    /**
1790
     * Validates and marshals a value to storage.
1791
     *
1792
     * @param array  $property property definition
1793
     * @param string $name     property name
1794
     * @param mixed  $value
1795
     *
1796
     * @return bool
1797
     */
1798
    private function filterAndValidate(array $property, $name, &$value)
1799
    {
1800
        // assume empty string is a null value for properties
1801
        // that are marked as optionally-null
1802
        if ($property['null'] && empty($value)) {
1803
            $value = null;
1804
1805
            return true;
1806
        }
1807
1808
        // validate
1809
        list($valid, $value) = $this->validateValue($property, $name, $value);
1810
1811
        // unique?
1812
        if ($valid && $property['unique'] && (false === $this->_id || $value != $this->ignoreUnsaved()->$name)) {
1813
            $valid = $this->checkUniqueness($property, $name, $value);
1814
        }
1815
1816
        return $valid;
1817
    }
1818
1819
    /**
1820
     * Validates a value for a property.
1821
     *
1822
     * @param array  $property property definition
1823
     * @param string $name     property name
1824
     * @param mixed  $value
1825
     *
1826
     * @return array
1827
     */
1828
    private function validateValue(array $property, $name, $value)
1829
    {
1830
        $valid = true;
1831
1832
        $error = 'pulsar.validation.failed';
1833
        if (isset($property['validate']) && is_callable($property['validate'])) {
1834
            $valid = call_user_func_array($property['validate'], [$value]);
1835
        } elseif (isset($property['validate'])) {
1836
            $validator = new Validator($property['validate']);
1837
            $valid = $validator->validate($value);
1838
            $error = 'pulsar.validation.'.$validator->getFailingRule();
1839
        }
1840
1841
        if (!$valid) {
1842
            $params = [
1843
                'field' => $name,
1844
                'field_name' => $this->getPropertyTitle($property, $name),
1845
            ];
1846
            $this->getErrors()->add($error, $params);
1847
        }
1848
1849
        return [$valid, $value];
1850
    }
1851
1852
    /**
1853
     * Checks if a value is unique for a property.
1854
     *
1855
     * @param array  $property property definition
1856
     * @param string $name     property name
1857
     * @param mixed  $value
1858
     *
1859
     * @return bool
1860
     */
1861
    private function checkUniqueness(array $property, $name, $value)
1862
    {
1863
        $n = static::query()->where([$name => $value])->count();
1864
        if ($n > 0) {
1865
            $params = [
1866
                'field' => $name,
1867
                'field_name' => $this->getPropertyTitle($property, $name),
1868
            ];
1869
            $this->getErrors()->add('pulsar.validation.unique', $params);
1870
1871
            return false;
1872
        }
1873
1874
        return true;
1875
    }
1876
1877
    /**
1878
     * Gets the marshaled default value for a property (if set).
1879
     *
1880
     * @param array $property
1881
     *
1882
     * @return mixed
1883
     */
1884
    private function getPropertyDefault(array $property)
1885
    {
1886
        return array_value($property, 'default');
1887
    }
1888
1889
    /**
1890
     * Gets the humanized name of a property.
1891
     *
1892
     * @param array  $property property definition
1893
     * @param string $name     property name
1894
     *
1895
     * @return string
1896
     */
1897
    private function getPropertyTitle(array $property, $name)
1898
    {
1899
        // look up the property from the locale service first
1900
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
1901
        $locale = $this->getErrors()->getLocale();
1902
        $title = $locale->t($k);
1903
        if ($title != $k) {
1904
            return $title;
1905
        }
1906
1907
        // DEPRECATED
1908
        if (isset($property['title'])) {
1909
            return $property['title'];
1910
        }
1911
1912
        // otherwise just attempt to title-ize the property name
1913
        return Inflector::get()->titleize($name);
1914
    }
1915
}
1916