Completed
Push — master ( 3f3907...70cb28 )
by Jared
01:46
created

Model::clearRelation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use 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
abstract class Model implements \ArrayAccess
32
{
33
    const IMMUTABLE = 0;
34
    const MUTABLE_CREATE_ONLY = 1;
35
    const MUTABLE = 2;
36
37
    const TYPE_STRING = 'string';
38
    const TYPE_NUMBER = 'number'; // DEPRECATED
39
    const TYPE_INTEGER = 'integer';
40
    const TYPE_FLOAT = 'float';
41
    const TYPE_BOOLEAN = 'boolean';
42
    const TYPE_DATE = 'date';
43
    const TYPE_OBJECT = 'object';
44
    const TYPE_ARRAY = 'array';
45
46
    const RELATIONSHIP_HAS_ONE = 'has_one';
47
    const RELATIONSHIP_HAS_MANY = 'has_many';
48
    const RELATIONSHIP_BELONGS_TO = 'belongs_to';
49
    const RELATIONSHIP_BELONGS_TO_MANY = 'belongs_to_many';
50
51
    const DEFAULT_ID_PROPERTY = 'id';
52
53
    /////////////////////////////
54
    // Model visible variables
55
    /////////////////////////////
56
57
    /**
58
     * List of model ID property names.
59
     *
60
     * @var array
61
     */
62
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
63
64
    /**
65
     * Property definitions expressed as a key-value map with
66
     * property names as the keys.
67
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
68
     *
69
     * @var array
70
     */
71
    protected static $properties = [];
72
73
    /**
74
     * @var array
75
     */
76
    protected static $dispatchers;
77
78
    /**
79
     * @var number|string|false
80
     */
81
    protected $_id;
82
83
    /**
84
     * @var array
85
     */
86
    protected $_ids;
87
88
    /**
89
     * @var array
90
     */
91
    protected $_values = [];
92
93
    /**
94
     * @var array
95
     */
96
    protected $_unsaved = [];
97
98
    /**
99
     * @var bool
100
     */
101
    protected $_persisted = false;
102
103
    /**
104
     * @var bool
105
     */
106
    protected $_loaded = false;
107
108
    /**
109
     * @var array
110
     */
111
    protected $_relationships = [];
112
113
    /**
114
     * @var Errors
115
     */
116
    protected $_errors;
117
118
    /////////////////////////////
119
    // Base model variables
120
    /////////////////////////////
121
122
    /**
123
     * @var array
124
     */
125
    private static $propertyDefinitionBase = [
126
        'type' => null,
127
        'mutable' => self::MUTABLE,
128
        'null' => false,
129
        'unique' => false,
130
        'required' => false,
131
    ];
132
133
    /**
134
     * @var array
135
     */
136
    private static $defaultIDProperty = [
137
        'type' => self::TYPE_INTEGER,
138
        'mutable' => self::IMMUTABLE,
139
    ];
140
141
    /**
142
     * @var array
143
     */
144
    private static $timestampProperties = [
145
        'created_at' => [
146
            'type' => self::TYPE_DATE,
147
            'validate' => 'timestamp|db_timestamp',
148
        ],
149
        'updated_at' => [
150
            'type' => self::TYPE_DATE,
151
            'validate' => 'timestamp|db_timestamp',
152
        ],
153
    ];
154
155
    /**
156
     * @var array
157
     */
158
    private static $softDeleteProperties = [
159
        'deleted_at' => [
160
            'type' => self::TYPE_DATE,
161
            'validate' => 'timestamp|db_timestamp',
162
            'null' => true,
163
        ],
164
    ];
165
166
    /**
167
     * @var array
168
     */
169
    private static $initialized = [];
170
171
    /**
172
     * @var DriverInterface
173
     */
174
    private static $driver;
175
176
    /**
177
     * @var array
178
     */
179
    private static $accessors = [];
180
181
    /**
182
     * @var array
183
     */
184
    private static $mutators = [];
185
186
    /**
187
     * @var bool
188
     */
189
    private $_ignoreUnsaved;
190
191
    /**
192
     * Creates a new model object.
193
     *
194
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
195
     * @param array                    $values optional key-value map to pre-seed model
196
     */
197
    public function __construct($id = false, array $values = [])
198
    {
199
        // initialize the model
200
        $this->init();
201
202
        // parse the supplied model ID
203
        $this->parseId($id);
204
205
        // load any given values
206
        if (count($values) > 0) {
207
            $this->refreshWith($values);
208
        }
209
    }
210
211
    /**
212
     * Performs initialization on this model.
213
     */
214
    private function init()
215
    {
216
        // ensure the initialize function is called only once
217
        $k = get_called_class();
218
        if (!isset(self::$initialized[$k])) {
219
            $this->initialize();
220
            self::$initialized[$k] = true;
221
        }
222
    }
223
224
    /**
225
     * The initialize() method is called once per model. It's used
226
     * to perform any one-off tasks before the model gets
227
     * constructed. This is a great place to add any model
228
     * properties. When extending this method be sure to call
229
     * parent::initialize() as some important stuff happens here.
230
     * If extending this method to add properties then you should
231
     * call parent::initialize() after adding any properties.
232
     */
233
    protected function initialize()
234
    {
235
        // load the driver
236
        static::getDriver();
237
238
        // add in the default ID property
239
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
240
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
241
        }
242
243
        // generates created_at and updated_at timestamps
244
        if (property_exists($this, 'autoTimestamps')) {
245
            $this->installAutoTimestamps();
246
        }
247
248
        // generates deleted_at timestamps
249
        if (property_exists($this, 'softDelete')) {
250
            $this->installSoftDelete();
251
        }
252
253
        // fill in each property by extending the property
254
        // definition base
255
        foreach (static::$properties as $k => &$property) {
256
            $property = array_replace(self::$propertyDefinitionBase, $property);
257
258
            // populate relationship property settings
259
            if (isset($property['relation'])) {
260
                // this is added for BC with older versions of pulsar
261
                // that only supported belongs to relationships
262
                if (!isset($property['relation_type'])) {
263
                    $property['relation_type'] = self::RELATIONSHIP_BELONGS_TO;
264
                    $property['local_key'] = $k;
265
                }
266
267
                $relation = $this->getRelationshipManager($property, $k);
268
                if (!isset($property['foreign_key'])) {
269
                    $property['foreign_key'] = $relation->getForeignKey();
270
                }
271
272
                if (!isset($property['local_key'])) {
273
                    $property['local_key'] = $relation->getLocalKey();
274
                }
275
276
                if (!isset($property['pivot_tablename']) && $relation instanceof BelongsToMany) {
277
                    $property['pivot_tablename'] = $relation->getTablename();
278
                }
279
            }
280
        }
281
282
        // order the properties array by name for consistency
283
        // since it is constructed in a random order
284
        ksort(static::$properties);
285
    }
286
287
    /**
288
     * Installs the `created_at` and `updated_at` properties.
289
     */
290
    private function installAutoTimestamps()
291
    {
292
        static::$properties = array_replace(self::$timestampProperties, static::$properties);
293
294
        self::creating(function (ModelEvent $event) {
295
            $model = $event->getModel();
296
            $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...
297
            $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...
298
        });
299
300
        self::updating(function (ModelEvent $event) {
301
            $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...
302
        });
303
    }
304
305
    /**
306
     * Installs the `deleted_at` properties.
307
     */
308
    private function installSoftDelete()
309
    {
310
        static::$properties = array_replace(self::$softDeleteProperties, static::$properties);
311
    }
312
313
    /**
314
     * Parses the given ID, which can be a single or composite primary key.
315
     *
316
     * @param mixed $id
317
     */
318
    private function parseId($id)
319
    {
320
        if (is_array($id)) {
321
            // A model can be supplied as a primary key
322
            foreach ($id as &$el) {
323
                if ($el instanceof self) {
324
                    $el = $el->id();
325
                }
326
            }
327
328
            // The IDs come in as the same order as ::$ids.
329
            // We need to match up the elements on that
330
            // input into a key-value map for each ID property.
331
            $ids = [];
332
            $idQueue = array_reverse($id);
333
            foreach (static::$ids as $k => $f) {
334
                // type cast
335
                if (count($idQueue) > 0) {
336
                    $idProperty = static::getProperty($f);
337
                    $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 336 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...
338
                } else {
339
                    $ids[$f] = false;
340
                }
341
            }
342
343
            $this->_id = implode(',', $id);
344
            $this->_ids = $ids;
345
        } elseif ($id instanceof self) {
346
            // A model can be supplied as a primary key
347
            $this->_id = $id->id();
348
            $this->_ids = $id->ids();
349
        } else {
350
            // type cast the single primary key
351
            $idName = static::$ids[0];
352
            if (false !== $id) {
353
                $idProperty = static::getProperty($idName);
354
                $id = static::cast($idProperty, $id);
0 ignored issues
show
Bug introduced by
It seems like $idProperty defined by static::getProperty($idName) on line 353 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...
355
            }
356
357
            $this->_id = $id;
358
            $this->_ids = [$idName => $id];
359
        }
360
    }
361
362
    /**
363
     * Sets the driver for all models.
364
     *
365
     * @param DriverInterface $driver
366
     */
367
    public static function setDriver(DriverInterface $driver)
368
    {
369
        self::$driver = $driver;
370
    }
371
372
    /**
373
     * Gets the driver for all models.
374
     *
375
     * @return DriverInterface
376
     *
377
     * @throws DriverMissingException when a driver has not been set yet
378
     */
379
    public static function getDriver()
380
    {
381
        if (!self::$driver) {
382
            throw new DriverMissingException('A model driver has not been set yet.');
383
        }
384
385
        return self::$driver;
386
    }
387
388
    /**
389
     * Clears the driver for all models.
390
     */
391
    public static function clearDriver()
392
    {
393
        self::$driver = null;
394
    }
395
396
    /**
397
     * Gets the name of the model, i.e. User.
398
     *
399
     * @return string
400
     */
401
    public static function modelName()
402
    {
403
        // strip namespacing
404
        $paths = explode('\\', get_called_class());
405
406
        return end($paths);
407
    }
408
409
    /**
410
     * Gets the model ID.
411
     *
412
     * @return string|number|false ID
413
     */
414
    public function id()
415
    {
416
        return $this->_id;
417
    }
418
419
    /**
420
     * Gets a key-value map of the model ID.
421
     *
422
     * @return array ID map
423
     */
424
    public function ids()
425
    {
426
        return $this->_ids;
427
    }
428
429
    /////////////////////////////
430
    // Magic Methods
431
    /////////////////////////////
432
433
    /**
434
     * Converts the model into a string.
435
     *
436
     * @return string
437
     */
438
    public function __toString()
439
    {
440
        $values = array_merge($this->_values, $this->_unsaved, $this->_ids);
441
        ksort($values);
442
443
        return get_called_class().'('.json_encode($values, JSON_PRETTY_PRINT).')';
444
    }
445
446
    /**
447
     * Shortcut to a get() call for a given property.
448
     *
449
     * @param string $name
450
     *
451
     * @return mixed
452
     */
453
    public function __get($name)
454
    {
455
        $result = $this->get([$name]);
456
457
        return reset($result);
458
    }
459
460
    /**
461
     * Sets an unsaved value.
462
     *
463
     * @param string $name
464
     * @param mixed  $value
465
     */
466
    public function __set($name, $value)
467
    {
468
        // if changing property, remove relation model
469
        if (isset($this->_relationships[$name])) {
470
            unset($this->_relationships[$name]);
471
        }
472
473
        // call any mutators
474
        $mutator = self::getMutator($name);
475
        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...
476
            $this->_unsaved[$name] = $this->$mutator($value);
477
        } else {
478
            $this->_unsaved[$name] = $value;
479
        }
480
    }
481
482
    /**
483
     * Checks if an unsaved value or property exists by this name.
484
     *
485
     * @param string $name
486
     *
487
     * @return bool
488
     */
489
    public function __isset($name)
490
    {
491
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
492
    }
493
494
    /**
495
     * Unsets an unsaved value.
496
     *
497
     * @param string $name
498
     */
499
    public function __unset($name)
500
    {
501
        if (array_key_exists($name, $this->_unsaved)) {
502
            // if changing property, remove relation model
503
            if (isset($this->_relationships[$name])) {
504
                unset($this->_relationships[$name]);
505
            }
506
507
            unset($this->_unsaved[$name]);
508
        }
509
    }
510
511
    /////////////////////////////
512
    // ArrayAccess Interface
513
    /////////////////////////////
514
515
    public function offsetExists($offset)
516
    {
517
        return isset($this->$offset);
518
    }
519
520
    public function offsetGet($offset)
521
    {
522
        return $this->$offset;
523
    }
524
525
    public function offsetSet($offset, $value)
526
    {
527
        $this->$offset = $value;
528
    }
529
530
    public function offsetUnset($offset)
531
    {
532
        unset($this->$offset);
533
    }
534
535
    public static function __callStatic($name, $parameters)
536
    {
537
        // Any calls to unkown static methods should be deferred to
538
        // the query. This allows calls like User::where()
539
        // to replace User::query()->where().
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
540
        return call_user_func_array([static::query(), $name], $parameters);
541
    }
542
543
    /////////////////////////////
544
    // Property Definitions
545
    /////////////////////////////
546
547
    /**
548
     * Gets all the property definitions for the model.
549
     *
550
     * @return array key-value map of properties
551
     */
552
    public static function getProperties()
553
    {
554
        return static::$properties;
555
    }
556
557
    /**
558
     * Gets a property defition for the model.
559
     *
560
     * @param string $property property to lookup
561
     *
562
     * @return array|null property
563
     */
564
    public static function getProperty($property)
565
    {
566
        return array_value(static::$properties, $property);
567
    }
568
569
    /**
570
     * Gets the names of the model ID properties.
571
     *
572
     * @return array
573
     */
574
    public static function getIDProperties()
575
    {
576
        return static::$ids;
577
    }
578
579
    /**
580
     * Checks if the model has a property.
581
     *
582
     * @param string $property property
583
     *
584
     * @return bool has property
585
     */
586
    public static function hasProperty($property)
587
    {
588
        return isset(static::$properties[$property]);
589
    }
590
591
    /**
592
     * Gets the mutator method name for a given proeprty name.
593
     * Looks for methods in the form of `setPropertyValue`.
594
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
595
     *
596
     * @param string $property property
597
     *
598
     * @return string|false method name if it exists
599
     */
600 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...
601
    {
602
        $class = get_called_class();
603
604
        $k = $class.':'.$property;
605
        if (!array_key_exists($k, self::$mutators)) {
606
            $inflector = Inflector::get();
607
            $method = 'set'.$inflector->camelize($property).'Value';
608
609
            if (!method_exists($class, $method)) {
610
                $method = false;
611
            }
612
613
            self::$mutators[$k] = $method;
614
        }
615
616
        return self::$mutators[$k];
617
    }
618
619
    /**
620
     * Gets the accessor method name for a given proeprty name.
621
     * Looks for methods in the form of `getPropertyValue`.
622
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
623
     *
624
     * @param string $property property
625
     *
626
     * @return string|false method name if it exists
627
     */
628 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...
629
    {
630
        $class = get_called_class();
631
632
        $k = $class.':'.$property;
633
        if (!array_key_exists($k, self::$accessors)) {
634
            $inflector = Inflector::get();
635
            $method = 'get'.$inflector->camelize($property).'Value';
636
637
            if (!method_exists($class, $method)) {
638
                $method = false;
639
            }
640
641
            self::$accessors[$k] = $method;
642
        }
643
644
        return self::$accessors[$k];
645
    }
646
647
    /**
648
     * Marshals a value for a given property from storage.
649
     *
650
     * @param array $property
651
     * @param mixed $value
652
     *
653
     * @return mixed type-casted value
654
     */
655
    public static function cast(array $property, $value)
656
    {
657
        if (null === $value) {
658
            return;
659
        }
660
661
        // handle empty strings as null
662
        if ($property['null'] && '' == $value) {
663
            return;
664
        }
665
666
        $type = array_value($property, 'type');
667
        $m = 'to_'.$type;
668
669
        if (!method_exists(Property::class, $m)) {
670
            return $value;
671
        }
672
673
        return Property::$m($value);
674
    }
675
676
    /////////////////////////////
677
    // CRUD Operations
678
    /////////////////////////////
679
680
    /**
681
     * Gets the tablename for storing this model.
682
     *
683
     * @return string
684
     */
685
    public function getTablename()
686
    {
687
        $inflector = Inflector::get();
688
689
        return $inflector->camelize($inflector->pluralize(static::modelName()));
690
    }
691
692
    /**
693
     * Gets the ID of the connection in the connection manager
694
     * that stores this model.
695
     *
696
     * @return string|false
697
     */
698
    public function getConnection()
699
    {
700
        return false;
701
    }
702
703
    /**
704
     * Saves the model.
705
     *
706
     * @return bool true when the operation was successful
707
     */
708
    public function save()
709
    {
710
        if (false === $this->_id) {
711
            return $this->create();
712
        }
713
714
        return $this->set();
715
    }
716
717
    /**
718
     * Saves the model. Throws an exception when the operation fails.
719
     *
720
     * @throws ModelException when the model cannot be saved
721
     */
722
    public function saveOrFail()
723
    {
724
        if (!$this->save()) {
725
            $msg = 'Failed to save '.static::modelName();
726
            if ($validationErrors = $this->getErrors()->all()) {
727
                $msg .= ': '.implode(', ', $validationErrors);
728
            }
729
730
            throw new ModelException($msg);
731
        }
732
    }
733
734
    /**
735
     * Creates a new model.
736
     *
737
     * @param array $data optional key-value properties to set
738
     *
739
     * @return bool true when the operation was successful
740
     *
741
     * @throws BadMethodCallException when called on an existing model
742
     */
743
    public function create(array $data = [])
744
    {
745
        if (false !== $this->_id) {
746
            throw new BadMethodCallException('Cannot call create() on an existing model');
747
        }
748
749
        // mass assign values passed into create()
750
        $this->setValues($data);
751
752
        // clear any previous errors
753
        $this->getErrors()->clear();
754
755
        // dispatch the model.creating event
756
        if (!$this->performDispatch(ModelEvent::CREATING)) {
757
            return false;
758
        }
759
760
        $requiredProperties = [];
761
        foreach (static::$properties as $name => $property) {
762
            // build a list of the required properties
763
            if ($property['required']) {
764
                $requiredProperties[] = $name;
765
            }
766
767
            // add in default values
768
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
769
                $this->_unsaved[$name] = $property['default'];
770
            }
771
        }
772
773
        // validate the values being saved
774
        $validated = true;
775
        $insertArray = [];
776
        foreach ($this->_unsaved as $name => $value) {
777
            // exclude if value does not map to a property
778
            if (!isset(static::$properties[$name])) {
779
                continue;
780
            }
781
782
            $property = static::$properties[$name];
783
784
            // cannot insert immutable values
785
            // (unless using the default value)
786
            if (self::IMMUTABLE == $property['mutable'] && $value !== $this->getPropertyDefault($property)) {
787
                continue;
788
            }
789
790
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
791
            $insertArray[$name] = $value;
792
        }
793
794
        // check for required fields
795
        foreach ($requiredProperties as $name) {
796
            if (!isset($insertArray[$name])) {
797
                $property = static::$properties[$name];
798
                $params = [
799
                    'field' => $name,
800
                    'field_name' => $this->getPropertyTitle($property, $name),
801
                ];
802
                $this->getErrors()->add('pulsar.validation.required', $params);
803
804
                $validated = false;
805
            }
806
        }
807
808
        if (!$validated) {
809
            return false;
810
        }
811
812
        $created = self::$driver->createModel($this, $insertArray);
813
814 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...
815
            // determine the model's new ID
816
            $this->getNewID();
817
818
            // store the persisted values to the in-memory cache
819
            $this->_unsaved = [];
820
            $this->refreshWith(array_replace($this->_ids, $insertArray));
821
822
            // dispatch the model.created event
823
            if (!$this->performDispatch(ModelEvent::CREATED)) {
824
                return false;
825
            }
826
        }
827
828
        return $created;
829
    }
830
831
    /**
832
     * Ignores unsaved values when fetching the next value.
833
     *
834
     * @return $this
835
     */
836
    public function ignoreUnsaved()
837
    {
838
        $this->_ignoreUnsaved = true;
839
840
        return $this;
841
    }
842
843
    /**
844
     * Fetches property values from the model.
845
     *
846
     * This method looks up values in this order:
847
     * IDs, local cache, unsaved values, storage layer, defaults
848
     *
849
     * @param array $properties list of property names to fetch values of
850
     *
851
     * @return array
852
     */
853
    public function get(array $properties)
854
    {
855
        // load the values from the IDs and local model cache
856
        $values = array_replace($this->ids(), $this->_values);
857
858
        // unless specified, use any unsaved values
859
        $ignoreUnsaved = $this->_ignoreUnsaved;
860
        $this->_ignoreUnsaved = false;
861
862
        if (!$ignoreUnsaved) {
863
            $values = array_replace($values, $this->_unsaved);
864
        }
865
866
        // see if there are any model properties that do not exist.
867
        // when true then this means the model needs to be hydrated
868
        // NOTE: only looking at model properties and excluding dynamic/non-existent properties
869
        $modelProperties = array_keys(static::$properties);
870
        $numMissing = count(array_intersect($modelProperties, array_diff($properties, array_keys($values))));
871
872
        if ($numMissing > 0 && !$this->_loaded) {
873
            // load the model from the storage layer, if needed
874
            $this->refresh();
875
876
            $values = array_replace($values, $this->_values);
877
878
            if (!$ignoreUnsaved) {
879
                $values = array_replace($values, $this->_unsaved);
880
            }
881
        }
882
883
        // build a key-value map of the requested properties
884
        $return = [];
885
        foreach ($properties as $k) {
886
            $return[$k] = $this->getValue($k, $values);
887
        }
888
889
        return $return;
890
    }
891
892
    /**
893
     * Gets a property value from the model.
894
     *
895
     * Values are looked up in this order:
896
     *  1. unsaved values
897
     *  2. local values
898
     *  3. default value
899
     *  4. null
900
     *
901
     * @param string $property
902
     * @param array  $values
903
     *
904
     * @return mixed
905
     */
906
    protected function getValue($property, array $values)
907
    {
908
        $value = null;
909
910
        if (array_key_exists($property, $values)) {
911
            $value = $values[$property];
912
        } elseif (static::hasProperty($property)) {
913
            $value = $this->_values[$property] = $this->getPropertyDefault(static::$properties[$property]);
914
        }
915
916
        // call any accessors
917
        if ($accessor = self::getAccessor($property)) {
918
            $value = $this->$accessor($value);
919
        }
920
921
        return $value;
922
    }
923
924
    /**
925
     * Populates a newly created model with its ID.
926
     */
927
    protected function getNewID()
928
    {
929
        $ids = [];
930
        $namedIds = [];
931
        foreach (static::$ids as $k) {
932
            // attempt use the supplied value if the ID property is mutable
933
            $property = static::getProperty($k);
934
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
935
                $id = $this->_unsaved[$k];
936
            } else {
937
                $id = self::$driver->getCreatedID($this, $k);
938
            }
939
940
            $ids[] = $id;
941
            $namedIds[$k] = $id;
942
        }
943
944
        $this->_id = implode(',', $ids);
945
        $this->_ids = $namedIds;
946
    }
947
948
    /**
949
     * Sets a collection values on the model from an untrusted input.
950
     *
951
     * @param array $values
952
     *
953
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
954
     *
955
     * @return $this
956
     */
957
    public function setValues($values)
958
    {
959
        // check if the model has a mass assignment whitelist
960
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
961
962
        // if no whitelist, then check for a blacklist
963
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
964
965
        foreach ($values as $k => $value) {
966
            // check for mass assignment violations
967
            if (($permitted && !in_array($k, $permitted)) ||
968
                ($protected && in_array($k, $protected))) {
969
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
970
            }
971
972
            $this->$k = $value;
973
        }
974
975
        return $this;
976
    }
977
978
    /**
979
     * Converts the model to an array.
980
     *
981
     * @return array
982
     */
983
    public function toArray()
984
    {
985
        // build the list of properties to retrieve
986
        $properties = array_keys(static::$properties);
987
988
        // remove any hidden properties
989
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
990
        $properties = array_diff($properties, $hide);
991
992
        // add any appended properties
993
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
994
        $properties = array_merge($properties, $append);
995
996
        // get the values for the properties
997
        $result = $this->get($properties);
998
999
        foreach ($result as $k => &$value) {
1000
            // convert any models to arrays
1001
            if ($value instanceof self) {
1002
                $value = $value->toArray();
1003
            }
1004
        }
1005
1006
        // DEPRECATED
1007
        // apply the transformation hook
1008
        if (method_exists($this, 'toArrayHook')) {
1009
            $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...
1010
        }
1011
1012
        return $result;
1013
    }
1014
1015
    /**
1016
     * Updates the model.
1017
     *
1018
     * @param array $data optional key-value properties to set
1019
     *
1020
     * @return bool true when the operation was successful
1021
     *
1022
     * @throws BadMethodCallException when not called on an existing model
1023
     */
1024
    public function set(array $data = [])
1025
    {
1026
        if (false === $this->_id) {
1027
            throw new BadMethodCallException('Can only call set() on an existing model');
1028
        }
1029
1030
        // mass assign values passed into set()
1031
        $this->setValues($data);
1032
1033
        // clear any previous errors
1034
        $this->getErrors()->clear();
1035
1036
        // not updating anything?
1037
        if (0 == count($this->_unsaved)) {
1038
            return true;
1039
        }
1040
1041
        // dispatch the model.updating event
1042
        if (!$this->performDispatch(ModelEvent::UPDATING)) {
1043
            return false;
1044
        }
1045
1046
        // DEPRECATED
1047
        if (method_exists($this, 'preSetHook') && !$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...
1048
            return false;
1049
        }
1050
1051
        // validate the values being saved
1052
        $validated = true;
1053
        $updateArray = [];
1054
        foreach ($this->_unsaved as $name => $value) {
1055
            // exclude if value does not map to a property
1056
            if (!isset(static::$properties[$name])) {
1057
                continue;
1058
            }
1059
1060
            $property = static::$properties[$name];
1061
1062
            // can only modify mutable properties
1063
            if (self::MUTABLE != $property['mutable']) {
1064
                continue;
1065
            }
1066
1067
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
1068
            $updateArray[$name] = $value;
1069
        }
1070
1071
        if (!$validated) {
1072
            return false;
1073
        }
1074
1075
        $updated = self::$driver->updateModel($this, $updateArray);
1076
1077 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...
1078
            // store the persisted values to the in-memory cache
1079
            $this->_unsaved = [];
1080
            $this->refreshWith(array_replace($this->_values, $updateArray));
1081
1082
            // dispatch the model.updated event
1083
            if (!$this->performDispatch(ModelEvent::UPDATED)) {
1084
                return false;
1085
            }
1086
        }
1087
1088
        return $updated;
1089
    }
1090
1091
    /**
1092
     * Delete the model.
1093
     *
1094
     * @return bool true when the operation was successful
1095
     */
1096
    public function delete()
1097
    {
1098
        if (false === $this->_id) {
1099
            throw new BadMethodCallException('Can only call delete() on an existing model');
1100
        }
1101
1102
        // clear any previous errors
1103
        $this->getErrors()->clear();
1104
1105
        // dispatch the model.deleting event
1106
        if (!$this->performDispatch(ModelEvent::DELETING)) {
1107
            return false;
1108
        }
1109
1110
        // perform a hard (default) or soft delete
1111
        $hardDelete = true;
1112
        if (property_exists($this, 'softDelete')) {
1113
            $t = time();
1114
            $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...
1115
            $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...
1116
            $deleted = self::$driver->updateModel($this, ['deleted_at' => $t]);
1117
            $hardDelete = false;
1118
        } else {
1119
            $deleted = self::$driver->deleteModel($this);
1120
        }
1121
1122
        if ($deleted) {
1123
            // dispatch the model.deleted event
1124
            if (!$this->performDispatch(ModelEvent::DELETED)) {
1125
                return false;
1126
            }
1127
1128
            if ($hardDelete) {
1129
                $this->_persisted = false;
1130
            }
1131
        }
1132
1133
        return $deleted;
1134
    }
1135
1136
    /**
1137
     * Restores a soft-deleted model.
1138
     *
1139
     * @return bool
1140
     */
1141
    public function restore()
1142
    {
1143
        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...
1144
            throw new BadMethodCallException('Can only call restore() on a soft-deleted model');
1145
        }
1146
1147
        // dispatch the model.updating event
1148
        if (!$this->performDispatch(ModelEvent::UPDATING)) {
1149
            return false;
1150
        }
1151
1152
        $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...
1153
        $restored = self::$driver->updateModel($this, ['deleted_at' => null]);
1154
1155
        if ($restored) {
1156
            // dispatch the model.updated event
1157
            if (!$this->performDispatch(ModelEvent::UPDATED)) {
1158
                return false;
1159
            }
1160
        }
1161
1162
        return $restored;
1163
    }
1164
1165
    /**
1166
     * Checks if the model has been deleted.
1167
     *
1168
     * @return bool
1169
     */
1170
    public function isDeleted()
1171
    {
1172
        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...
1173
            return true;
1174
        }
1175
1176
        return !$this->_persisted;
1177
    }
1178
1179
    /////////////////////////////
1180
    // Queries
1181
    /////////////////////////////
1182
1183
    /**
1184
     * Generates a new query instance.
1185
     *
1186
     * @return Query
1187
     */
1188
    public static function query()
1189
    {
1190
        // Create a new model instance for the query to ensure
1191
        // that the model's initialize() method gets called.
1192
        // Otherwise, the property definitions will be incomplete.
1193
        $model = new static();
1194
        $query = new Query($model);
1195
1196
        // scope soft-deleted models to only include non-deleted models
1197
        if (property_exists($model, 'softDelete')) {
1198
            $query->where('deleted_at IS NOT NULL');
1199
        }
1200
1201
        return $query;
1202
    }
1203
1204
    /**
1205
     * Generates a new query instance that includes soft-deleted models.
1206
     *
1207
     * @return Query
1208
     */
1209
    public static function withDeleted()
1210
    {
1211
        // Create a new model instance for the query to ensure
1212
        // that the model's initialize() method gets called.
1213
        // Otherwise, the property definitions will be incomplete.
1214
        $model = new static();
1215
1216
        return new Query($model);
1217
    }
1218
1219
    /**
1220
     * Finds a single instance of a model given it's ID.
1221
     *
1222
     * @param mixed $id
1223
     *
1224
     * @return static|null
1225
     */
1226
    public static function find($id)
1227
    {
1228
        $ids = [];
1229
        $id = (array) $id;
1230
        foreach (static::$ids as $j => $k) {
1231
            if ($_id = array_value($id, $j)) {
1232
                $ids[$k] = $_id;
1233
            }
1234
        }
1235
1236
        // malformed ID
1237
        if (count($ids) < count(static::$ids)) {
1238
            return null;
1239
        }
1240
1241
        return static::query()->where($ids)->first();
1242
    }
1243
1244
    /**
1245
     * Finds a single instance of a model given it's ID or throws an exception.
1246
     *
1247
     * @param mixed $id
1248
     *
1249
     * @return static
1250
     *
1251
     * @throws ModelNotFoundException when a model could not be found
1252
     */
1253
    public static function findOrFail($id)
1254
    {
1255
        $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 1260 which is incompatible with the return type documented by Pulsar\Model::findOrFail of type Pulsar\Model.
Loading history...
1256
        if (!$model) {
1257
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1258
        }
1259
1260
        return $model;
1261
    }
1262
1263
    /**
1264
     * @deprecated
1265
     *
1266
     * Checks if the model exists in the database
1267
     *
1268
     * @return bool
1269
     */
1270
    public function exists()
1271
    {
1272
        return 1 == static::query()->where($this->ids())->count();
1273
    }
1274
1275
    /**
1276
     * Tells if this model instance has been persisted to the data layer.
1277
     *
1278
     * NOTE: this does not actually perform a check with the data layer
1279
     *
1280
     * @return bool
1281
     */
1282
    public function persisted()
1283
    {
1284
        return $this->_persisted;
1285
    }
1286
1287
    /**
1288
     * Loads the model from the storage layer.
1289
     *
1290
     * @return $this
1291
     */
1292
    public function refresh()
1293
    {
1294
        if (false === $this->_id) {
1295
            return $this;
1296
        }
1297
1298
        $values = self::$driver->loadModel($this);
1299
1300
        if (!is_array($values)) {
1301
            return $this;
1302
        }
1303
1304
        // clear any relations
1305
        $this->_relationships = [];
1306
1307
        return $this->refreshWith($values);
1308
    }
1309
1310
    /**
1311
     * Loads values into the model.
1312
     *
1313
     * @param array $values values
1314
     *
1315
     * @return $this
1316
     */
1317
    public function refreshWith(array $values)
1318
    {
1319
        // type cast the values
1320
        foreach ($values as $k => &$value) {
1321
            if ($property = static::getProperty($k)) {
1322
                $value = static::cast($property, $value);
1323
            }
1324
        }
1325
1326
        $this->_loaded = true;
1327
        $this->_persisted = true;
1328
        $this->_values = $values;
1329
1330
        return $this;
1331
    }
1332
1333
    /**
1334
     * Clears the cache for this model.
1335
     *
1336
     * @return $this
1337
     */
1338
    public function clearCache()
1339
    {
1340
        $this->_loaded = false;
1341
        $this->_unsaved = [];
1342
        $this->_values = [];
1343
        $this->_relationships = [];
1344
1345
        return $this;
1346
    }
1347
1348
    /////////////////////////////
1349
    // Relationships
1350
    /////////////////////////////
1351
1352
    /**
1353
     * @deprecated
1354
     *
1355
     * Gets the model(s) for a relationship
1356
     *
1357
     * @param string $k property
1358
     *
1359
     * @throws \InvalidArgumentException when the relationship manager cannot be created
1360
     *
1361
     * @return Model|null
1362
     */
1363
    public function relation($k)
1364
    {
1365
        if (!array_key_exists($k, $this->_relationships)) {
1366
            $property = static::getProperty($k);
1367
            $relation = $this->getRelationshipManager($property, $k);
0 ignored issues
show
Bug introduced by
It seems like $property defined by static::getProperty($k) on line 1366 can also be of type null; however, Pulsar\Model::getRelationshipManager() 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...
1368
            $this->_relationships[$k] = $relation->getResults();
1369
        }
1370
1371
        return $this->_relationships[$k];
1372
    }
1373
1374
    /**
1375
     * @deprecated
1376
     *
1377
     * Sets the model for a one-to-one relationship (has-one or belongs-to)
1378
     *
1379
     * @param string $k
1380
     * @param Model  $model
1381
     *
1382
     * @return $this
1383
     */
1384
    public function setRelation($k, self $model)
1385
    {
1386
        $this->$k = $model->id();
1387
        $this->_relationships[$k] = $model;
1388
1389
        return $this;
1390
    }
1391
1392
    /**
1393
     * @deprecated
1394
     *
1395
     * Sets the model for a one-to-many relationship
1396
     *
1397
     * @param string   $k
1398
     * @param iterable $models
1399
     *
1400
     * @return $this
1401
     */
1402
    public function setRelationCollection($k, iterable $models)
1403
    {
1404
        $this->_relationships[$k] = $models;
1405
1406
        return $this;
1407
    }
1408
1409
    /**
1410
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null.
1411
     *
1412
     * @param string $k
1413
     *
1414
     * @return $this
1415
     */
1416
    public function clearRelation($k)
1417
    {
1418
        $this->$k = null;
1419
        $this->_relationships[$k] = null;
1420
1421
        return $this;
1422
    }
1423
1424
    /**
1425
     * Builds a relationship manager object for a given property.
1426
     *
1427
     * @param array  $property
1428
     * @param string $name
1429
     *
1430
     * @throws \InvalidArgumentException when the relationship manager cannot be created
1431
     *
1432
     * @return Relation
1433
     */
1434
    public function getRelationshipManager(array $property, $name)
1435
    {
1436
        if (!isset($property['relation'])) {
1437
            throw new \InvalidArgumentException('Property "'.$name.'" does not have a relationship.');
1438
        }
1439
1440
        $relationModelClass = $property['relation'];
1441
        $foreignKey = array_value($property, 'foreign_key');
1442
        $localKey = array_value($property, 'local_key');
1443
1444
        if (self::RELATIONSHIP_HAS_ONE == $property['relation_type']) {
1445
            return $this->hasOne($relationModelClass, $foreignKey, $localKey);
1446
        }
1447
1448
        if (self::RELATIONSHIP_HAS_MANY == $property['relation_type']) {
1449
            return $this->hasMany($relationModelClass, $foreignKey, $localKey);
1450
        }
1451
1452
        if (self::RELATIONSHIP_BELONGS_TO == $property['relation_type']) {
1453
            return $this->belongsTo($relationModelClass, $foreignKey, $localKey);
1454
        }
1455
1456
        if (self::RELATIONSHIP_BELONGS_TO_MANY == $property['relation_type']) {
1457
            $pivotTable = array_value($property, 'pivot_tablename');
1458
1459
            return $this->belongsToMany($relationModelClass, $pivotTable, $foreignKey, $localKey);
1460
        }
1461
1462
        throw new \InvalidArgumentException('Relationship type on "'.$name.'" property not supported: '.$property['relation_type']);
1463
    }
1464
1465
    /**
1466
     * Creates the parent side of a One-To-One relationship.
1467
     *
1468
     * @param string $model      foreign model class
1469
     * @param string $foreignKey identifying key on foreign model
1470
     * @param string $localKey   identifying key on local model
1471
     *
1472
     * @return HasOne
1473
     */
1474
    public function hasOne($model, $foreignKey = '', $localKey = '')
1475
    {
1476
        return new HasOne($this, $localKey, $model, $foreignKey);
1477
    }
1478
1479
    /**
1480
     * Creates the child side of a One-To-One or One-To-Many relationship.
1481
     *
1482
     * @param string $model      foreign model class
1483
     * @param string $foreignKey identifying key on foreign model
1484
     * @param string $localKey   identifying key on local model
1485
     *
1486
     * @return BelongsTo
1487
     */
1488
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1489
    {
1490
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1491
    }
1492
1493
    /**
1494
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1495
     *
1496
     * @param string $model      foreign model class
1497
     * @param string $foreignKey identifying key on foreign model
1498
     * @param string $localKey   identifying key on local model
1499
     *
1500
     * @return HasMany
1501
     */
1502
    public function hasMany($model, $foreignKey = '', $localKey = '')
1503
    {
1504
        return new HasMany($this, $localKey, $model, $foreignKey);
1505
    }
1506
1507
    /**
1508
     * Creates the child side of a Many-To-Many relationship.
1509
     *
1510
     * @param string $model      foreign model class
1511
     * @param string $tablename  pivot table name
1512
     * @param string $foreignKey identifying key on foreign model
1513
     * @param string $localKey   identifying key on local model
1514
     *
1515
     * @return BelongsToMany
1516
     */
1517
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1518
    {
1519
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1520
    }
1521
1522
    /////////////////////////////
1523
    // Events
1524
    /////////////////////////////
1525
1526
    /**
1527
     * Gets the event dispatcher.
1528
     *
1529
     * @return EventDispatcher
1530
     */
1531
    public static function getDispatcher($ignoreCache = false)
1532
    {
1533
        $class = get_called_class();
1534
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1535
            self::$dispatchers[$class] = new EventDispatcher();
1536
        }
1537
1538
        return self::$dispatchers[$class];
1539
    }
1540
1541
    /**
1542
     * Subscribes to a listener to an event.
1543
     *
1544
     * @param string   $event    event name
1545
     * @param callable $listener
1546
     * @param int      $priority optional priority, higher #s get called first
1547
     */
1548
    public static function listen($event, callable $listener, $priority = 0)
1549
    {
1550
        static::getDispatcher()->addListener($event, $listener, $priority);
1551
    }
1552
1553
    /**
1554
     * Adds a listener to the model.creating and model.updating events.
1555
     *
1556
     * @param callable $listener
1557
     * @param int      $priority
1558
     */
1559
    public static function saving(callable $listener, $priority = 0)
1560
    {
1561
        static::listen(ModelEvent::CREATING, $listener, $priority);
1562
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1563
    }
1564
1565
    /**
1566
     * Adds a listener to the model.created and model.updated events.
1567
     *
1568
     * @param callable $listener
1569
     * @param int      $priority
1570
     */
1571
    public static function saved(callable $listener, $priority = 0)
1572
    {
1573
        static::listen(ModelEvent::CREATED, $listener, $priority);
1574
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1575
    }
1576
1577
    /**
1578
     * Adds a listener to the model.creating event.
1579
     *
1580
     * @param callable $listener
1581
     * @param int      $priority
1582
     */
1583
    public static function creating(callable $listener, $priority = 0)
1584
    {
1585
        static::listen(ModelEvent::CREATING, $listener, $priority);
1586
    }
1587
1588
    /**
1589
     * Adds a listener to the model.created event.
1590
     *
1591
     * @param callable $listener
1592
     * @param int      $priority
1593
     */
1594
    public static function created(callable $listener, $priority = 0)
1595
    {
1596
        static::listen(ModelEvent::CREATED, $listener, $priority);
1597
    }
1598
1599
    /**
1600
     * Adds a listener to the model.updating event.
1601
     *
1602
     * @param callable $listener
1603
     * @param int      $priority
1604
     */
1605
    public static function updating(callable $listener, $priority = 0)
1606
    {
1607
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1608
    }
1609
1610
    /**
1611
     * Adds a listener to the model.updated event.
1612
     *
1613
     * @param callable $listener
1614
     * @param int      $priority
1615
     */
1616
    public static function updated(callable $listener, $priority = 0)
1617
    {
1618
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1619
    }
1620
1621
    /**
1622
     * Adds a listener to the model.deleting event.
1623
     *
1624
     * @param callable $listener
1625
     * @param int      $priority
1626
     */
1627
    public static function deleting(callable $listener, $priority = 0)
1628
    {
1629
        static::listen(ModelEvent::DELETING, $listener, $priority);
1630
    }
1631
1632
    /**
1633
     * Adds a listener to the model.deleted event.
1634
     *
1635
     * @param callable $listener
1636
     * @param int      $priority
1637
     */
1638
    public static function deleted(callable $listener, $priority = 0)
1639
    {
1640
        static::listen(ModelEvent::DELETED, $listener, $priority);
1641
    }
1642
1643
    /**
1644
     * Dispatches the given event and checks if it was successful.
1645
     *
1646
     * @param string $eventName
1647
     *
1648
     * @return bool true if the events were successfully propagated
1649
     */
1650
    private function performDispatch($eventName)
1651
    {
1652
        $event = new ModelEvent($this);
1653
        static::getDispatcher()->dispatch($eventName, $event);
1654
1655
        return !$event->isPropagationStopped();
1656
    }
1657
1658
    /////////////////////////////
1659
    // Validation
1660
    /////////////////////////////
1661
1662
    /**
1663
     * Gets the error stack for this model.
1664
     *
1665
     * @return Errors
1666
     */
1667
    public function getErrors()
1668
    {
1669
        if (!$this->_errors) {
1670
            $this->_errors = new Errors();
1671
        }
1672
1673
        return $this->_errors;
1674
    }
1675
1676
    /**
1677
     * Checks if the model in its current state is valid.
1678
     *
1679
     * @return bool
1680
     */
1681
    public function valid()
1682
    {
1683
        // clear any previous errors
1684
        $this->getErrors()->clear();
1685
1686
        // run the validator against the model values
1687
        $values = $this->_unsaved + $this->_values;
1688
1689
        $validated = true;
1690
        foreach ($values as $k => $v) {
1691
            $property = static::getProperty($k);
1692
            $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 1691 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...
1693
        }
1694
1695
        // add back any modified unsaved values
1696
        foreach (array_keys($this->_unsaved) as $k) {
1697
            $this->_unsaved[$k] = $values[$k];
1698
        }
1699
1700
        return $validated;
1701
    }
1702
1703
    /**
1704
     * Validates and marshals a value to storage.
1705
     *
1706
     * @param array  $property property definition
1707
     * @param string $name     property name
1708
     * @param mixed  $value
1709
     *
1710
     * @return bool
1711
     */
1712
    private function filterAndValidate(array $property, $name, &$value)
1713
    {
1714
        // assume empty string is a null value for properties
1715
        // that are marked as optionally-null
1716
        if ($property['null'] && empty($value)) {
1717
            $value = null;
1718
1719
            return true;
1720
        }
1721
1722
        // validate
1723
        list($valid, $value) = $this->validateValue($property, $name, $value);
1724
1725
        // unique?
1726
        if ($valid && $property['unique'] && (false === $this->_id || $value != $this->ignoreUnsaved()->$name)) {
1727
            $valid = $this->checkUniqueness($property, $name, $value);
1728
        }
1729
1730
        return $valid;
1731
    }
1732
1733
    /**
1734
     * Validates a value for a property.
1735
     *
1736
     * @param array  $property property definition
1737
     * @param string $name     property name
1738
     * @param mixed  $value
1739
     *
1740
     * @return array
1741
     */
1742
    private function validateValue(array $property, $name, $value)
1743
    {
1744
        $valid = true;
1745
1746
        $error = 'pulsar.validation.failed';
1747
        if (isset($property['validate']) && is_callable($property['validate'])) {
1748
            $valid = call_user_func_array($property['validate'], [$value]);
1749
        } elseif (isset($property['validate'])) {
1750
            $validator = new Validator($property['validate']);
1751
            $valid = $validator->validate($value);
1752
            $error = 'pulsar.validation.'.$validator->getFailingRule();
1753
        }
1754
1755
        if (!$valid) {
1756
            $params = [
1757
                'field' => $name,
1758
                'field_name' => $this->getPropertyTitle($property, $name),
1759
            ];
1760
            $this->getErrors()->add($error, $params);
1761
        }
1762
1763
        return [$valid, $value];
1764
    }
1765
1766
    /**
1767
     * Checks if a value is unique for a property.
1768
     *
1769
     * @param array  $property property definition
1770
     * @param string $name     property name
1771
     * @param mixed  $value
1772
     *
1773
     * @return bool
1774
     */
1775
    private function checkUniqueness(array $property, $name, $value)
1776
    {
1777
        $n = static::query()->where([$name => $value])->count();
1778
        if ($n > 0) {
1779
            $params = [
1780
                'field' => $name,
1781
                'field_name' => $this->getPropertyTitle($property, $name),
1782
            ];
1783
            $this->getErrors()->add('pulsar.validation.unique', $params);
1784
1785
            return false;
1786
        }
1787
1788
        return true;
1789
    }
1790
1791
    /**
1792
     * Gets the marshaled default value for a property (if set).
1793
     *
1794
     * @param array $property
1795
     *
1796
     * @return mixed
1797
     */
1798
    private function getPropertyDefault(array $property)
1799
    {
1800
        return array_value($property, 'default');
1801
    }
1802
1803
    /**
1804
     * Gets the humanized name of a property.
1805
     *
1806
     * @param array  $property property definition
1807
     * @param string $name     property name
1808
     *
1809
     * @return string
1810
     */
1811
    private function getPropertyTitle(array $property, $name)
1812
    {
1813
        // look up the property from the locale service first
1814
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
1815
        $locale = $this->getErrors()->getLocale();
1816
        $title = $locale->t($k);
1817
        if ($title != $k) {
1818
            return $title;
1819
        }
1820
1821
        // DEPRECATED
1822
        if (isset($property['title'])) {
1823
            return $property['title'];
1824
        }
1825
1826
        // otherwise just attempt to title-ize the property name
1827
        return Inflector::get()->titleize($name);
1828
    }
1829
}
1830