Completed
Push — master ( 624a8f...285d67 )
by Jared
01:31
created

Model::exists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
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
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
     * Sets the model for a one-to-one relationship (has-one or belongs-to) as null.
1394
     *
1395
     * @param string $k
1396
     *
1397
     * @return $this
1398
     */
1399
    public function clearRelation($k)
1400
    {
1401
        $this->$k = null;
1402
        $this->_relationships[$k] = null;
1403
1404
        return $this;
1405
    }
1406
1407
    /**
1408
     * Builds a relationship manager object for a given property.
1409
     *
1410
     * @param array  $property
1411
     * @param string $name
1412
     *
1413
     * @throws \InvalidArgumentException when the relationship manager cannot be created
1414
     *
1415
     * @return Relation
1416
     */
1417
    public function getRelationshipManager(array $property, $name)
1418
    {
1419
        if (!isset($property['relation'])) {
1420
            throw new \InvalidArgumentException('Property "'.$name.'" does not have a relationship.');
1421
        }
1422
1423
        $relationModelClass = $property['relation'];
1424
        $foreignKey = array_value($property, 'foreign_key');
1425
        $localKey = array_value($property, 'local_key');
1426
1427
        if (self::RELATIONSHIP_HAS_ONE == $property['relation_type']) {
1428
            return $this->hasOne($relationModelClass, $foreignKey, $localKey);
1429
        }
1430
1431
        if (self::RELATIONSHIP_HAS_MANY == $property['relation_type']) {
1432
            return $this->hasMany($relationModelClass, $foreignKey, $localKey);
1433
        }
1434
1435
        if (self::RELATIONSHIP_BELONGS_TO == $property['relation_type']) {
1436
            return $this->belongsTo($relationModelClass, $foreignKey, $localKey);
1437
        }
1438
1439
        if (self::RELATIONSHIP_BELONGS_TO_MANY == $property['relation_type']) {
1440
            $pivotTable = array_value($property, 'pivot_tablename');
1441
1442
            return $this->belongsToMany($relationModelClass, $pivotTable, $foreignKey, $localKey);
1443
        }
1444
1445
        throw new \InvalidArgumentException('Relationship type on "'.$name.'" property not supported: '.$property['relation_type']);
1446
    }
1447
1448
    /**
1449
     * Creates the parent side of a One-To-One relationship.
1450
     *
1451
     * @param string $model      foreign model class
1452
     * @param string $foreignKey identifying key on foreign model
1453
     * @param string $localKey   identifying key on local model
1454
     *
1455
     * @return HasOne
1456
     */
1457
    public function hasOne($model, $foreignKey = '', $localKey = '')
1458
    {
1459
        return new HasOne($this, $localKey, $model, $foreignKey);
1460
    }
1461
1462
    /**
1463
     * Creates the child side of a One-To-One or One-To-Many relationship.
1464
     *
1465
     * @param string $model      foreign model class
1466
     * @param string $foreignKey identifying key on foreign model
1467
     * @param string $localKey   identifying key on local model
1468
     *
1469
     * @return BelongsTo
1470
     */
1471
    public function belongsTo($model, $foreignKey = '', $localKey = '')
1472
    {
1473
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1474
    }
1475
1476
    /**
1477
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1478
     *
1479
     * @param string $model      foreign model class
1480
     * @param string $foreignKey identifying key on foreign model
1481
     * @param string $localKey   identifying key on local model
1482
     *
1483
     * @return HasMany
1484
     */
1485
    public function hasMany($model, $foreignKey = '', $localKey = '')
1486
    {
1487
        return new HasMany($this, $localKey, $model, $foreignKey);
1488
    }
1489
1490
    /**
1491
     * Creates the child side of a Many-To-Many relationship.
1492
     *
1493
     * @param string $model      foreign model class
1494
     * @param string $tablename  pivot table name
1495
     * @param string $foreignKey identifying key on foreign model
1496
     * @param string $localKey   identifying key on local model
1497
     *
1498
     * @return BelongsToMany
1499
     */
1500
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1501
    {
1502
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1503
    }
1504
1505
    /////////////////////////////
1506
    // Events
1507
    /////////////////////////////
1508
1509
    /**
1510
     * Gets the event dispatcher.
1511
     *
1512
     * @return EventDispatcher
1513
     */
1514
    public static function getDispatcher($ignoreCache = false)
1515
    {
1516
        $class = get_called_class();
1517
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1518
            self::$dispatchers[$class] = new EventDispatcher();
1519
        }
1520
1521
        return self::$dispatchers[$class];
1522
    }
1523
1524
    /**
1525
     * Subscribes to a listener to an event.
1526
     *
1527
     * @param string   $event    event name
1528
     * @param callable $listener
1529
     * @param int      $priority optional priority, higher #s get called first
1530
     */
1531
    public static function listen($event, callable $listener, $priority = 0)
1532
    {
1533
        static::getDispatcher()->addListener($event, $listener, $priority);
1534
    }
1535
1536
    /**
1537
     * Adds a listener to the model.creating and model.updating events.
1538
     *
1539
     * @param callable $listener
1540
     * @param int      $priority
1541
     */
1542
    public static function saving(callable $listener, $priority = 0)
1543
    {
1544
        static::listen(ModelEvent::CREATING, $listener, $priority);
1545
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1546
    }
1547
1548
    /**
1549
     * Adds a listener to the model.created and model.updated events.
1550
     *
1551
     * @param callable $listener
1552
     * @param int      $priority
1553
     */
1554
    public static function saved(callable $listener, $priority = 0)
1555
    {
1556
        static::listen(ModelEvent::CREATED, $listener, $priority);
1557
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1558
    }
1559
1560
    /**
1561
     * Adds a listener to the model.creating event.
1562
     *
1563
     * @param callable $listener
1564
     * @param int      $priority
1565
     */
1566
    public static function creating(callable $listener, $priority = 0)
1567
    {
1568
        static::listen(ModelEvent::CREATING, $listener, $priority);
1569
    }
1570
1571
    /**
1572
     * Adds a listener to the model.created event.
1573
     *
1574
     * @param callable $listener
1575
     * @param int      $priority
1576
     */
1577
    public static function created(callable $listener, $priority = 0)
1578
    {
1579
        static::listen(ModelEvent::CREATED, $listener, $priority);
1580
    }
1581
1582
    /**
1583
     * Adds a listener to the model.updating event.
1584
     *
1585
     * @param callable $listener
1586
     * @param int      $priority
1587
     */
1588
    public static function updating(callable $listener, $priority = 0)
1589
    {
1590
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1591
    }
1592
1593
    /**
1594
     * Adds a listener to the model.updated event.
1595
     *
1596
     * @param callable $listener
1597
     * @param int      $priority
1598
     */
1599
    public static function updated(callable $listener, $priority = 0)
1600
    {
1601
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1602
    }
1603
1604
    /**
1605
     * Adds a listener to the model.deleting event.
1606
     *
1607
     * @param callable $listener
1608
     * @param int      $priority
1609
     */
1610
    public static function deleting(callable $listener, $priority = 0)
1611
    {
1612
        static::listen(ModelEvent::DELETING, $listener, $priority);
1613
    }
1614
1615
    /**
1616
     * Adds a listener to the model.deleted event.
1617
     *
1618
     * @param callable $listener
1619
     * @param int      $priority
1620
     */
1621
    public static function deleted(callable $listener, $priority = 0)
1622
    {
1623
        static::listen(ModelEvent::DELETED, $listener, $priority);
1624
    }
1625
1626
    /**
1627
     * Dispatches the given event and checks if it was successful.
1628
     *
1629
     * @param string $eventName
1630
     *
1631
     * @return bool true if the events were successfully propagated
1632
     */
1633
    private function performDispatch($eventName)
1634
    {
1635
        $event = new ModelEvent($this);
1636
        static::getDispatcher()->dispatch($eventName, $event);
1637
1638
        return !$event->isPropagationStopped();
1639
    }
1640
1641
    /////////////////////////////
1642
    // Validation
1643
    /////////////////////////////
1644
1645
    /**
1646
     * Gets the error stack for this model.
1647
     *
1648
     * @return Errors
1649
     */
1650
    public function getErrors()
1651
    {
1652
        if (!$this->_errors) {
1653
            $this->_errors = new Errors();
1654
        }
1655
1656
        return $this->_errors;
1657
    }
1658
1659
    /**
1660
     * Checks if the model in its current state is valid.
1661
     *
1662
     * @return bool
1663
     */
1664
    public function valid()
1665
    {
1666
        // clear any previous errors
1667
        $this->getErrors()->clear();
1668
1669
        // run the validator against the model values
1670
        $values = $this->_unsaved + $this->_values;
1671
1672
        $validated = true;
1673
        foreach ($values as $k => $v) {
1674
            $property = static::getProperty($k);
1675
            $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 1674 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...
1676
        }
1677
1678
        // add back any modified unsaved values
1679
        foreach (array_keys($this->_unsaved) as $k) {
1680
            $this->_unsaved[$k] = $values[$k];
1681
        }
1682
1683
        return $validated;
1684
    }
1685
1686
    /**
1687
     * Validates and marshals a value to storage.
1688
     *
1689
     * @param array  $property property definition
1690
     * @param string $name     property name
1691
     * @param mixed  $value
1692
     *
1693
     * @return bool
1694
     */
1695
    private function filterAndValidate(array $property, $name, &$value)
1696
    {
1697
        // assume empty string is a null value for properties
1698
        // that are marked as optionally-null
1699
        if ($property['null'] && empty($value)) {
1700
            $value = null;
1701
1702
            return true;
1703
        }
1704
1705
        // validate
1706
        list($valid, $value) = $this->validateValue($property, $name, $value);
1707
1708
        // unique?
1709
        if ($valid && $property['unique'] && (false === $this->_id || $value != $this->ignoreUnsaved()->$name)) {
1710
            $valid = $this->checkUniqueness($property, $name, $value);
1711
        }
1712
1713
        return $valid;
1714
    }
1715
1716
    /**
1717
     * Validates a value for a property.
1718
     *
1719
     * @param array  $property property definition
1720
     * @param string $name     property name
1721
     * @param mixed  $value
1722
     *
1723
     * @return array
1724
     */
1725
    private function validateValue(array $property, $name, $value)
1726
    {
1727
        $valid = true;
1728
1729
        $error = 'pulsar.validation.failed';
1730
        if (isset($property['validate']) && is_callable($property['validate'])) {
1731
            $valid = call_user_func_array($property['validate'], [$value]);
1732
        } elseif (isset($property['validate'])) {
1733
            $validator = new Validator($property['validate']);
1734
            $valid = $validator->validate($value);
1735
            $error = 'pulsar.validation.'.$validator->getFailingRule();
1736
        }
1737
1738
        if (!$valid) {
1739
            $params = [
1740
                'field' => $name,
1741
                'field_name' => $this->getPropertyTitle($property, $name),
1742
            ];
1743
            $this->getErrors()->add($error, $params);
1744
        }
1745
1746
        return [$valid, $value];
1747
    }
1748
1749
    /**
1750
     * Checks if a value is unique for a property.
1751
     *
1752
     * @param array  $property property definition
1753
     * @param string $name     property name
1754
     * @param mixed  $value
1755
     *
1756
     * @return bool
1757
     */
1758
    private function checkUniqueness(array $property, $name, $value)
1759
    {
1760
        $n = static::query()->where([$name => $value])->count();
1761
        if ($n > 0) {
1762
            $params = [
1763
                'field' => $name,
1764
                'field_name' => $this->getPropertyTitle($property, $name),
1765
            ];
1766
            $this->getErrors()->add('pulsar.validation.unique', $params);
1767
1768
            return false;
1769
        }
1770
1771
        return true;
1772
    }
1773
1774
    /**
1775
     * Gets the marshaled default value for a property (if set).
1776
     *
1777
     * @param array $property
1778
     *
1779
     * @return mixed
1780
     */
1781
    private function getPropertyDefault(array $property)
1782
    {
1783
        return array_value($property, 'default');
1784
    }
1785
1786
    /**
1787
     * Gets the humanized name of a property.
1788
     *
1789
     * @param array  $property property definition
1790
     * @param string $name     property name
1791
     *
1792
     * @return string
1793
     */
1794
    private function getPropertyTitle(array $property, $name)
1795
    {
1796
        // look up the property from the locale service first
1797
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
1798
        $locale = $this->getErrors()->getLocale();
1799
        $title = $locale->t($k);
1800
        if ($title != $k) {
1801
            return $title;
1802
        }
1803
1804
        // DEPRECATED
1805
        if (isset($property['title'])) {
1806
            return $property['title'];
1807
        }
1808
1809
        // otherwise just attempt to title-ize the property name
1810
        return Inflector::get()->titleize($name);
1811
    }
1812
}
1813