Completed
Push — master ( 9e2a7d...173c6d )
by Jared
06:54 queued 03:34
created

Model::getApp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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