Completed
Push — master ( 09fdfc...151ae4 )
by Jared
02:07
created

Model::refreshWith()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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