Completed
Push — master ( 8c952a...39aa42 )
by Jared
02:55
created

Model   F

Complexity

Total Complexity 160

Size/Duplication

Total Lines 1327
Duplicated Lines 8.29 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 46
Bugs 5 Features 5
Metric Value
wmc 160
c 46
b 5
f 5
lcom 1
cbo 16
dl 110
loc 1327
rs 0.6314

67 Methods

Rating   Name   Duplication   Size   Complexity  
B setValues() 0 20 9
A ignoreUnsaved() 0 6 1
C get() 0 43 8
A __construct() 0 13 3
B initialize() 0 14 5
A installAutoTimestamps() 0 17 2
A setDriver() 0 4 1
A getDriver() 0 8 2
A clearDriver() 0 4 1
A setLocale() 0 4 1
A getLocale() 0 4 1
A clearLocale() 0 4 1
A modelName() 0 4 1
A id() 0 12 2
A ids() 0 4 1
A __toString() 0 4 1
A __get() 0 4 1
A __set() 0 4 1
A __isset() 0 4 2
A __unset() 0 10 3
A __callStatic() 0 7 1
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A offsetSet() 0 4 1
A offsetUnset() 0 4 1
A getIdProperties() 0 4 1
A buildFromId() 0 12 2
A getMutator() 18 18 3
A getAccessor() 18 18 3
A isRelationship() 0 4 1
A getDateFormat() 0 8 2
A getPropertyTitle() 0 12 4
A getPropertyType() 0 6 2
C cast() 14 49 12
A getProperties() 0 5 1
A hasProperty() 0 5 2
B setValue() 0 25 5
B toArray() 0 31 6
A save() 0 8 2
B create() 0 39 5
A getNewIds() 0 15 3
B set() 0 38 6
B delete() 0 26 5
A persisted() 0 4 1
A refresh() 0 17 3
A refreshWith() 0 17 4
A query() 0 9 1
A find() 0 6 1
A findOrFail() 0 9 2
A totalRecords() 0 7 1
A hasOne() 15 15 3
A belongsTo() 15 15 3
A hasMany() 15 15 3
A belongsToMany() 15 15 3
A loadRelationship() 0 9 2
A getDispatcher() 0 9 3
A listen() 0 4 1
A creating() 0 4 1
A created() 0 4 1
A updating() 0 4 1
A updated() 0 4 1
A deleting() 0 4 1
A deleted() 0 4 1
A dispatch() 0 6 1
A errors() 0 8 2
A valid() 0 17 2
A getValidator() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
namespace Pulsar;
12
13
use BadMethodCallException;
14
use Carbon\Carbon;
15
use ICanBoogie\Inflector;
16
use Infuse\Locale;
17
use InvalidArgumentException;
18
use Pulsar\Driver\DriverInterface;
19
use Pulsar\Exception\DriverMissingException;
20
use Pulsar\Exception\MassAssignmentException;
21
use Pulsar\Exception\NotFoundException;
22
use Pulsar\Relation\HasOne;
23
use Pulsar\Relation\BelongsTo;
24
use Pulsar\Relation\HasMany;
25
use Pulsar\Relation\BelongsToMany;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
28
abstract class Model implements \ArrayAccess
29
{
30
    const TYPE_STRING = 'string';
31
    const TYPE_INTEGER = 'integer';
32
    const TYPE_FLOAT = 'float';
33
    const TYPE_BOOLEAN = 'boolean';
34
    const TYPE_DATE = 'date';
35
    const TYPE_OBJECT = 'object';
36
    const TYPE_ARRAY = 'array';
37
38
    const DEFAULT_ID_PROPERTY = 'id';
39
40
    const DEFAULT_DATE_FORMAT = 'U'; // unix timestamps
41
42
    /////////////////////////////
43
    // Model visible variables
44
    /////////////////////////////
45
46
    /**
47
     * List of model ID property names.
48
     *
49
     * @staticvar array
50
     */
51
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
52
53
    /**
54
     * Validation rules expressed as a key-value map with
55
     * property names as the keys.
56
     * i.e. ['name' => 'string:2'].
57
     *
58
     * @staticvar array
59
     */
60
    protected static $validations = [];
61
62
    /**
63
     * @staticvar array
64
     */
65
    protected static $relationships = [];
66
67
    /**
68
     * @staticvar array
69
     */
70
    protected static $dates = [];
71
72
    /**
73
     * @staticvar array
74
     */
75
    protected static $dispatchers;
76
77
    /**
78
     * @var array
79
     */
80
    protected $_values = [];
81
82
    /**
83
     * @var array
84
     */
85
    protected $_unsaved = [];
86
87
    /**
88
     * @var bool
89
     */
90
    protected $_persisted = false;
91
92
    /**
93
     * @var Errors
94
     */
95
    protected $_errors;
96
97
    /////////////////////////////
98
    // Base model variables
99
    /////////////////////////////
100
101
    /**
102
     * @staticvar array
103
     */
104
    private static $initialized = [];
105
106
    /**
107
     * @staticvar DriverInterface
108
     */
109
    private static $driver;
110
111
    /**
112
     * @staticvar Locale
113
     */
114
    private static $locale;
115
116
    /**
117
     * @staticvar array
118
     */
119
    private static $accessors = [];
120
121
    /**
122
     * @staticvar array
123
     */
124
    private static $mutators = [];
125
126
    /**
127
     * @var bool
128
     */
129
    private $_ignoreUnsaved;
130
131
    /**
132
     * Creates a new model object.
133
     *
134
     * @param array $values values to fill model with
135
     */
136
    public function __construct(array $values = [])
137
    {
138
        foreach ($values as $k => $v) {
139
            $this->setValue($k, $v, false);
140
        }
141
142
        // ensure the initialize function is called only once
143
        $k = get_called_class();
144
        if (!isset(self::$initialized[$k])) {
145
            $this->initialize();
146
            self::$initialized[$k] = true;
147
        }
148
    }
149
150
    /**
151
     * The initialize() method is called once per model. It's used
152
     * to perform any one-off tasks before the model gets
153
     * constructed. This is a great place to add any model
154
     * properties. When extending this method be sure to call
155
     * parent::initialize() as some important stuff happens here.
156
     * If extending this method to add properties then you should
157
     * call parent::initialize() after adding any properties.
158
     */
159
    protected function initialize()
160
    {
161
        // add in the default ID property
162
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
163
            if (property_exists($this, 'casts') && !isset(static::$casts[self::DEFAULT_ID_PROPERTY])) {
164
                static::$casts[self::DEFAULT_ID_PROPERTY] = self::TYPE_INTEGER;
165
            }
166
        }
167
168
        // generates created_at and updated_at timestamps
169
        if (property_exists($this, 'autoTimestamps')) {
170
            $this->installAutoTimestamps();
171
        }
172
    }
173
174
    private function installAutoTimestamps()
175
    {
176
        if (property_exists($this, 'casts')) {
177
            static::$casts['created_at'] = self::TYPE_DATE;
178
            static::$casts['updated_at'] = self::TYPE_DATE;
179
        }
180
181
        self::creating(function (ModelEvent $event) {
182
            $model = $event->getModel();
183
            $model->created_at = Carbon::now();
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...
184
            $model->updated_at = Carbon::now();
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...
185
        });
186
187
        self::updating(function (ModelEvent $event) {
188
            $event->getModel()->updated_at = Carbon::now();
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...
189
        });
190
    }
191
192
    /**
193
     * Sets the driver for all models.
194
     *
195
     * @param DriverInterface $driver
196
     */
197
    public static function setDriver(DriverInterface $driver)
198
    {
199
        self::$driver = $driver;
200
    }
201
202
    /**
203
     * Gets the driver for all models.
204
     *
205
     * @return DriverInterface
206
     *
207
     * @throws DriverMissingException
208
     */
209
    public static function getDriver()
210
    {
211
        if (!self::$driver) {
212
            throw new DriverMissingException('A model driver has not been set yet.');
213
        }
214
215
        return self::$driver;
216
    }
217
218
    /**
219
     * Clears the driver for all models.
220
     */
221
    public static function clearDriver()
222
    {
223
        self::$driver = null;
224
    }
225
226
    /**
227
     * Sets the locale instance for all models.
228
     *
229
     * @param Locale $locale
230
     */
231
    public static function setLocale(Locale $locale)
232
    {
233
        self::$locale = $locale;
234
    }
235
236
    /**
237
     * Gets the locale instance for all models.
238
     *
239
     * @return Locale
240
     */
241
    public static function getLocale()
242
    {
243
        return self::$locale;
244
    }
245
246
    /**
247
     * Clears the locale for all models.
248
     */
249
    public static function clearLocale()
250
    {
251
        self::$locale = null;
252
    }
253
254
    /**
255
     * Gets the name of the model without namespacing.
256
     *
257
     * @return string
258
     */
259
    public static function modelName()
260
    {
261
        return explode('\\', get_called_class())[0];
262
    }
263
264
    /**
265
     * Gets the model ID.
266
     *
267
     * @return string|number|null ID
268
     */
269
    public function id()
270
    {
271
        $ids = $this->ids();
272
273
        // if a single ID then return it
274
        if (count($ids) === 1) {
275
            return reset($ids);
276
        }
277
278
        // if multiple IDs then return a comma-separated list
279
        return implode(',', $ids);
280
    }
281
282
    /**
283
     * Gets a key-value map of the model ID.
284
     *
285
     * @return array ID map
286
     */
287
    public function ids()
288
    {
289
        return $this->get(static::$ids);
290
    }
291
292
    /////////////////////////////
293
    // Magic Methods
294
    /////////////////////////////
295
296
    public function __toString()
297
    {
298
        return get_called_class().'('.$this->id().')';
299
    }
300
301
    public function __get($name)
302
    {
303
        return array_values($this->get([$name]))[0];
304
    }
305
306
    public function __set($name, $value)
307
    {
308
        $this->setValue($name, $value);
309
    }
310
311
    public function __isset($name)
312
    {
313
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
314
    }
315
316
    public function __unset($name)
317
    {
318
        if (static::isRelationship($name)) {
319
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
320
        }
321
322
        if (array_key_exists($name, $this->_unsaved)) {
323
            unset($this->_unsaved[$name]);
324
        }
325
    }
326
327
    public static function __callStatic($name, $parameters)
328
    {
329
        // Any calls to unkown static methods should be deferred to
330
        // the query. This allows calls like User::where()
331
        // 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...
332
        return call_user_func_array([static::query(), $name], $parameters);
333
    }
334
335
    /////////////////////////////
336
    // ArrayAccess Interface
337
    /////////////////////////////
338
339
    public function offsetExists($offset)
340
    {
341
        return isset($this->$offset);
342
    }
343
344
    public function offsetGet($offset)
345
    {
346
        return $this->$offset;
347
    }
348
349
    public function offsetSet($offset, $value)
350
    {
351
        $this->$offset = $value;
352
    }
353
354
    public function offsetUnset($offset)
355
    {
356
        unset($this->$offset);
357
    }
358
359
    /////////////////////////////
360
    // Property Definitions
361
    /////////////////////////////
362
363
    /**
364
     * Gets the names of the model ID properties.
365
     *
366
     * @return array
367
     */
368
    public static function getIdProperties()
369
    {
370
        return static::$ids;
371
    }
372
373
    /**
374
     * Builds an existing model instance given a single ID value or
375
     * ordered array of ID values.
376
     *
377
     * @param mixed $id
378
     *
379
     * @return Model
380
     */
381
    public static function buildFromId($id)
382
    {
383
        $ids = [];
384
        $id = (array) $id;
385
        foreach (static::$ids as $j => $k) {
386
            $ids[$k] = $id[$j];
387
        }
388
389
        $model = new static($ids);
390
391
        return $model;
392
    }
393
394
    /**
395
     * Gets the mutator method name for a given proeprty name.
396
     * Looks for methods in the form of `setPropertyValue`.
397
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
398
     *
399
     * @param string $property
400
     *
401
     * @return string|false method name if it exists
402
     */
403 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...
404
    {
405
        $class = get_called_class();
406
407
        $k = $class.':'.$property;
408
        if (!array_key_exists($k, self::$mutators)) {
409
            $inflector = Inflector::get();
410
            $method = 'set'.$inflector->camelize($property).'Value';
411
412
            if (!method_exists($class, $method)) {
413
                $method = false;
414
            }
415
416
            self::$mutators[$k] = $method;
417
        }
418
419
        return self::$mutators[$k];
420
    }
421
422
    /**
423
     * Gets the accessor method name for a given proeprty name.
424
     * Looks for methods in the form of `getPropertyValue`.
425
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
426
     *
427
     * @param string $property
428
     *
429
     * @return string|false method name if it exists
430
     */
431 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...
432
    {
433
        $class = get_called_class();
434
435
        $k = $class.':'.$property;
436
        if (!array_key_exists($k, self::$accessors)) {
437
            $inflector = Inflector::get();
438
            $method = 'get'.$inflector->camelize($property).'Value';
439
440
            if (!method_exists($class, $method)) {
441
                $method = false;
442
            }
443
444
            self::$accessors[$k] = $method;
445
        }
446
447
        return self::$accessors[$k];
448
    }
449
450
    /**
451
     * Checks if a given property is a relationship.
452
     *
453
     * @param string $property
454
     *
455
     * @return bool
456
     */
457
    public static function isRelationship($property)
458
    {
459
        return in_array($property, static::$relationships);
460
    }
461
462
    /**
463
     * Gets the string date format for a property. Defaults to
464
     * UNIX timestamps.
465
     *
466
     * @param string $property
467
     *
468
     * @return string
469
     */
470
    public static function getDateFormat($property)
471
    {
472
        if (isset(static::$dates[$property])) {
473
            return static::$dates[$property];
474
        }
475
476
        return self::DEFAULT_DATE_FORMAT;
477
    }
478
479
    /**
480
     * Gets the title of a property.
481
     *
482
     * @param string $name
483
     *
484
     * @return string
485
     */
486
    public static function getPropertyTitle($name)
487
    {
488
        // attmept to fetch the title from the Locale service
489
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
490
        if (self::$locale && $title = self::$locale->t($k)) {
491
            if ($title != $k) {
492
                return $title;
493
            }
494
        }
495
496
        return Inflector::get()->humanize($name);
497
    }
498
499
    /**
500
     * Gets the type cast for a property.
501
     *
502
     * @param string $property
503
     *
504
     * @return string|null
505
     */
506
    public static function getPropertyType($property)
507
    {
508
        if (property_exists(get_called_class(), 'casts')) {
509
            return array_value(static::$casts, $property);
510
        }
511
    }
512
513
    /**
514
     * Casts a value to a given type.
515
     *
516
     * @param string|null $type
517
     * @param mixed       $value
518
     * @param string      $property optional property name
519
     *
520
     * @return mixed casted value
521
     */
522
    public static function cast($type, $value, $property = null)
523
    {
524
        if ($value === null) {
525
            return;
526
        }
527
528
        switch ($type) {
529
        case self::TYPE_STRING:
530
            return (string) $value;
531
532
        case self::TYPE_INTEGER:
533
            return (int) $value;
534
535
        case self::TYPE_FLOAT:
536
            return (float) $value;
537
538
        case self::TYPE_BOOLEAN:
539
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
540
541
        case self::TYPE_DATE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
542
            // cast dates into Carbon objects
543
            if ($value instanceof Carbon) {
544
                return $value;
545
            } else {
546
                $format = self::getDateFormat($property);
547
548
                return Carbon::createFromFormat($format, $value);
549
            }
550
551 View Code Duplication
        case self::TYPE_ARRAY:
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...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
552
            // decode JSON into an array
553
            if (is_string($value)) {
554
                return json_decode($value, true);
555
            } else {
556
                return (array) $value;
557
            }
558
559 View Code Duplication
        case self::TYPE_OBJECT:
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...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
560
            // decode JSON into an object
561
            if (is_string($value)) {
562
                return (object) json_decode($value);
563
            } else {
564
                return (object) $value;
565
            }
566
567
        default:
568
            return $value;
569
        }
570
    }
571
572
    /**
573
     * Gets the properties of this model.
574
     *
575
     * @return array
576
     */
577
    public function getProperties()
578
    {
579
        return array_unique(array_merge(
580
            static::$ids, array_keys($this->_values)));
581
    }
582
583
    /**
584
     * Checks if the model has a property.
585
     *
586
     * @param string $property
587
     *
588
     * @return bool has property
589
     */
590
    public function hasProperty($property)
591
    {
592
        return array_key_exists($property, $this->_values) ||
593
               in_array($property, static::$ids);
594
    }
595
596
    /////////////////////////////
597
    // Values
598
    /////////////////////////////
599
600
    /**
601
     * Sets an unsaved value.
602
     *
603
     * @param string $name
604
     * @param mixed  $value
605
     * @param bool   $unsaved when true, sets an unsaved value
606
     *
607
     * @throws BadMethodCallException when setting a relationship
608
     *
609
     * @return self
610
     */
611
    public function setValue($name, $value, $unsaved = true)
612
    {
613
        if (static::isRelationship($name)) {
614
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
615
        }
616
617
        // cast the value
618
        if ($type = static::getPropertyType($name)) {
619
            $value = static::cast($type, $value, $name);
620
        }
621
622
        // apply any mutators
623
        if ($mutator = self::getMutator($name)) {
624
            $value = $this->$mutator($value);
625
        }
626
627
        // save the value on the model property
628
        if ($unsaved) {
629
            $this->_unsaved[$name] = $value;
630
        } else {
631
            $this->_values[$name] = $value;
632
        }
633
634
        return $this;
635
    }
636
637
    /**
638
     * Sets a collection values on the model from an untrusted
639
     * input. Also known as mass assignment.
640
     *
641
     * @param array $values
642
     *
643
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
644
     *
645
     * @return self
646
     */
647
    public function setValues($values)
648
    {
649
        // check if the model has a mass assignment whitelist
650
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
651
652
        // if no whitelist, then check for a blacklist
653
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
654
655
        foreach ($values as $k => $value) {
656
            // check for mass assignment violations
657
            if (($permitted && !in_array($k, $permitted)) ||
658
                ($protected && in_array($k, $protected))) {
659
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
660
            }
661
662
            $this->setValue($k, $value);
663
        }
664
665
        return $this;
666
    }
667
668
    /**
669
     * Ignores unsaved values when fetching the next value.
670
     *
671
     * @return self
672
     */
673
    public function ignoreUnsaved()
674
    {
675
        $this->_ignoreUnsaved = true;
676
677
        return $this;
678
    }
679
680
    /**
681
     * Gets property values from the model.
682
     *
683
     * This method looks up values from these locations in this
684
     * precedence order (least important to most important):
685
     *  1. local values
686
     *  2. unsaved values
687
     *
688
     * @param array $properties list of property names to fetch values of
689
     *
690
     * @return array
691
     *
692
     * @throws InvalidArgumentException when a property was requested not present in the values
693
     */
694
    public function get(array $properties)
695
    {
696
        // load the values from the local model cache
697
        $values = $this->_values;
698
699
        // unless specified, use any unsaved values
700
        $ignoreUnsaved = $this->_ignoreUnsaved;
701
        $this->_ignoreUnsaved = false;
702
        if (!$ignoreUnsaved) {
703
            $values = array_replace($values, $this->_unsaved);
704
        }
705
706
        // build the response
707
        $result = [];
708
        foreach ($properties as $k) {
709
            $accessor = self::getAccessor($k);
710
711
            // use the supplied value if it's available
712
            if (array_key_exists($k, $values)) {
713
                $result[$k] = $values[$k];
714
            // get relationship values
715
            } elseif (static::isRelationship($k)) {
716
                $result[$k] = $this->loadRelationship($k);
717
            // set any missing values to null
718
            } elseif ($this->hasProperty($k)) {
719
                $result[$k] = $this->_values[$k] = null;
720
            // throw an exception for non-properties that do not
721
            // have an accessor
722
            } elseif (!$accessor) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $accessor of type string|false is loosely compared to false; 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...
723
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
724
            // otherwise the value is considered null
725
            } else {
726
                $result[$k] = null;
727
            }
728
729
            // call any accessors
730
            if ($accessor) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $accessor 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...
731
                $result[$k] = $this->$accessor($result[$k]);
732
            }
733
        }
734
735
        return $result;
736
    }
737
738
    /**
739
     * Converts the model to an array.
740
     *
741
     * @return array model array
742
     */
743
    public function toArray()
744
    {
745
        // build the list of properties to retrieve
746
        $properties = $this->getProperties();
747
748
        // remove any hidden properties
749
        if (property_exists($this, 'hidden')) {
750
            $properties = array_diff($properties, static::$hidden);
751
        }
752
753
        // include any appended properties
754
        if (property_exists($this, 'appended')) {
755
            $properties = array_unique(array_merge($properties, static::$appended));
756
        }
757
758
        // get the values for the properties
759
        $result = $this->get($properties);
760
761
        foreach ($result as $k => &$value) {
762
            // convert any models to arrays
763
            if ($value instanceof self) {
764
                $value = $value->toArray();
765
            // convert any Carbon objects to date strings
766
            } elseif ($value instanceof Carbon) {
767
                $format = self::getDateFormat($k);
768
                $value = $value->format($format);
769
            }
770
        }
771
772
        return $result;
773
    }
774
775
    /////////////////////////////
776
    // Persistence
777
    /////////////////////////////
778
779
    /**
780
     * Saves the model.
781
     *
782
     * @return bool
783
     */
784
    public function save()
785
    {
786
        if (!$this->_persisted) {
787
            return $this->create();
788
        }
789
790
        return $this->set();
791
    }
792
793
    /**
794
     * Creates a new model.
795
     *
796
     * @param array $data optional key-value properties to set
797
     *
798
     * @return bool
799
     *
800
     * @throws BadMethodCallException when called on an existing model
801
     */
802
    public function create(array $data = [])
803
    {
804
        if ($this->_persisted) {
805
            throw new BadMethodCallException('Cannot call create() on an existing model');
806
        }
807
808
        // mass assign values passed into create()
809
        $this->setValues($data);
810
811
        // add in any preset values
812
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
813
814
        // dispatch the model.creating event
815
        $event = $this->dispatch(ModelEvent::CREATING);
816
        if ($event->isPropagationStopped()) {
817
            return false;
818
        }
819
820
        // validate the model
821
        if (!$this->valid()) {
822
            return false;
823
        }
824
825
        // create the model using the driver
826
        if (!self::getDriver()->createModel($this, $this->_unsaved)) {
827
            return false;
828
        }
829
830
        // update the model with the persisted values and new ID(s)
831
        $newValues = array_replace(
832
            $this->_unsaved,
833
            $this->getNewIds());
834
        $this->refreshWith($newValues);
835
836
        // dispatch the model.created event
837
        $event = $this->dispatch(ModelEvent::CREATED);
838
839
        return !$event->isPropagationStopped();
840
    }
841
842
    /**
843
     * Gets the IDs for a newly created model.
844
     *
845
     * @return string
846
     */
847
    protected function getNewIds()
848
    {
849
        $ids = [];
850
        foreach (static::$ids as $k) {
851
            // check if the ID property was already given,
852
            if (isset($this->_unsaved[$k])) {
853
                $ids[$k] = $this->_unsaved[$k];
854
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
855
            } else {
856
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
857
            }
858
        }
859
860
        return $ids;
861
    }
862
863
    /**
864
     * Updates the model.
865
     *
866
     * @param array $data optional key-value properties to set
867
     *
868
     * @return bool
869
     *
870
     * @throws BadMethodCallException when not called on an existing model
871
     */
872
    public function set(array $data = [])
873
    {
874
        if (!$this->_persisted) {
875
            throw new BadMethodCallException('Can only call set() on an existing model');
876
        }
877
878
        // mass assign values passed into set()
879
        $this->setValues($data);
880
881
        // not updating anything?
882
        if (count($this->_unsaved) === 0) {
883
            return true;
884
        }
885
886
        // dispatch the model.updating event
887
        $event = $this->dispatch(ModelEvent::UPDATING);
888
        if ($event->isPropagationStopped()) {
889
            return false;
890
        }
891
892
        // validate the model
893
        if (!$this->valid()) {
894
            return false;
895
        }
896
897
        // update the model using the driver
898
        if (!self::getDriver()->updateModel($this, $this->_unsaved)) {
899
            return false;
900
        }
901
902
        // update the model with the persisted values
903
        $this->refreshWith($this->_unsaved);
904
905
        // dispatch the model.updated event
906
        $event = $this->dispatch(ModelEvent::UPDATED);
907
908
        return !$event->isPropagationStopped();
909
    }
910
911
    /**
912
     * Delete the model.
913
     *
914
     * @return bool success
915
     */
916
    public function delete()
917
    {
918
        if (!$this->_persisted) {
919
            throw new BadMethodCallException('Can only call delete() on an existing model');
920
        }
921
922
        // dispatch the model.deleting event
923
        $event = $this->dispatch(ModelEvent::DELETING);
924
        if ($event->isPropagationStopped()) {
925
            return false;
926
        }
927
928
        $deleted = self::getDriver()->deleteModel($this);
929
930
        if ($deleted) {
931
            // dispatch the model.deleted event
932
            $event = $this->dispatch(ModelEvent::DELETED);
933
            if ($event->isPropagationStopped()) {
934
                return false;
935
            }
936
937
            $this->_persisted = false;
938
        }
939
940
        return $deleted;
941
    }
942
943
    /**
944
     * Tells if the model has been persisted.
945
     *
946
     * @return bool
947
     */
948
    public function persisted()
949
    {
950
        return $this->_persisted;
951
    }
952
953
    /**
954
     * Loads the model from the data layer.
955
     *
956
     * @return self
957
     *
958
     * @throws NotFoundException
959
     */
960
    public function refresh()
961
    {
962
        if (!$this->_persisted) {
963
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
964
        }
965
966
        $query = static::query();
967
        $query->where($this->ids());
968
969
        $values = self::getDriver()->queryModels($query);
970
971
        if (count($values) === 0) {
972
            return $this;
973
        }
974
975
        return $this->refreshWith($values[0]);
976
    }
977
978
    /**
979
     * Loads values into the model retrieved from the data layer.
980
     *
981
     * @param array $values values
982
     *
983
     * @return self
984
     */
985
    public function refreshWith(array $values)
986
    {
987
        // cast the values
988
        if (property_exists($this, 'casts')) {
989
            foreach ($values as $k => &$value) {
990
                if ($type = static::getPropertyType($k)) {
991
                    $value = static::cast($type, $value, $k);
992
                }
993
            }
994
        }
995
996
        $this->_persisted = true;
997
        $this->_values = $values;
998
        $this->_unsaved = [];
999
1000
        return $this;
1001
    }
1002
1003
    /////////////////////////////
1004
    // Queries
1005
    /////////////////////////////
1006
1007
    /**
1008
     * Generates a new query instance.
1009
     *
1010
     * @return Query
1011
     */
1012
    public static function query()
1013
    {
1014
        // Create a new model instance for the query to ensure
1015
        // that the model's initialize() method gets called.
1016
        // Otherwise, the property definitions will be incomplete.
1017
        $model = new static();
1018
1019
        return new Query($model);
1020
    }
1021
1022
    /**
1023
     * Finds a single instance of a model given it's ID.
1024
     *
1025
     * @param mixed $id
1026
     *
1027
     * @return Model|null
1028
     */
1029
    public static function find($id)
1030
    {
1031
        $model = static::buildFromId($id);
1032
1033
        return static::query()->where($model->ids())->first();
1034
    }
1035
1036
    /**
1037
     * Finds a single instance of a model given it's ID or throws an exception.
1038
     *
1039
     * @param mixed $id
1040
     *
1041
     * @return Model|false
1042
     *
1043
     * @throws NotFoundException when a model could not be found
1044
     */
1045
    public static function findOrFail($id)
1046
    {
1047
        $model = static::find($id);
1048
        if (!$model) {
1049
            throw new NotFoundException('Could not find the requested '.static::modelName());
1050
        }
1051
1052
        return $model;
1053
    }
1054
1055
    /**
1056
     * Gets the toal number of records matching an optional criteria.
1057
     *
1058
     * @param array $where criteria
1059
     *
1060
     * @return int total
1061
     */
1062
    public static function totalRecords(array $where = [])
1063
    {
1064
        $query = static::query();
1065
        $query->where($where);
1066
1067
        return self::getDriver()->totalRecords($query);
1068
    }
1069
1070
    /////////////////////////////
1071
    // Relationships
1072
    /////////////////////////////
1073
1074
    /**
1075
     * Creates the parent side of a One-To-One relationship.
1076
     *
1077
     * @param string $model      foreign model class
1078
     * @param string $foreignKey identifying key on foreign model
1079
     * @param string $localKey   identifying key on local model
1080
     *
1081
     * @return \Pulsar\Relation\Relation
1082
     */
1083 View Code Duplication
    public function hasOne($model, $foreignKey = '', $localKey = '')
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...
1084
    {
1085
        // the default local key would look like `user_id`
1086
        // for a model named User
1087
        if (!$foreignKey) {
1088
            $inflector = Inflector::get();
1089
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1090
        }
1091
1092
        if (!$localKey) {
1093
            $localKey = self::DEFAULT_ID_PROPERTY;
1094
        }
1095
1096
        return new HasOne($model, $foreignKey, $localKey, $this);
1097
    }
1098
1099
    /**
1100
     * Creates the child side of a One-To-One or One-To-Many relationship.
1101
     *
1102
     * @param string $model      foreign model class
1103
     * @param string $foreignKey identifying key on foreign model
1104
     * @param string $localKey   identifying key on local model
1105
     *
1106
     * @return \Pulsar\Relation\Relation
1107
     */
1108 View Code Duplication
    public function belongsTo($model, $foreignKey = '', $localKey = '')
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...
1109
    {
1110
        if (!$foreignKey) {
1111
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1112
        }
1113
1114
        // the default local key would look like `user_id`
1115
        // for a model named User
1116
        if (!$localKey) {
1117
            $inflector = Inflector::get();
1118
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1119
        }
1120
1121
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1122
    }
1123
1124
    /**
1125
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1126
     *
1127
     * @param string $model      foreign model class
1128
     * @param string $foreignKey identifying key on foreign model
1129
     * @param string $localKey   identifying key on local model
1130
     *
1131
     * @return \Pulsar\Relation\Relation
1132
     */
1133 View Code Duplication
    public function hasMany($model, $foreignKey = '', $localKey = '')
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...
1134
    {
1135
        // the default local key would look like `user_id`
1136
        // for a model named User
1137
        if (!$foreignKey) {
1138
            $inflector = Inflector::get();
1139
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1140
        }
1141
1142
        if (!$localKey) {
1143
            $localKey = self::DEFAULT_ID_PROPERTY;
1144
        }
1145
1146
        return new HasMany($model, $foreignKey, $localKey, $this);
1147
    }
1148
1149
    /**
1150
     * Creates the child side of a Many-To-Many relationship.
1151
     *
1152
     * @param string $model      foreign model class
1153
     * @param string $foreignKey identifying key on foreign model
1154
     * @param string $localKey   identifying key on local model
1155
     *
1156
     * @return \Pulsar\Relation\Relation
1157
     */
1158 View Code Duplication
    public function belongsToMany($model, $foreignKey = '', $localKey = '')
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...
1159
    {
1160
        if (!$foreignKey) {
1161
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1162
        }
1163
1164
        // the default local key would look like `user_id`
1165
        // for a model named User
1166
        if (!$localKey) {
1167
            $inflector = Inflector::get();
1168
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1169
        }
1170
1171
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1172
    }
1173
1174
    /**
1175
     * Loads a given relationship (if not already) and returns
1176
     * its results.
1177
     *
1178
     * @param string $name
1179
     *
1180
     * @return mixed
1181
     */
1182
    protected function loadRelationship($name)
1183
    {
1184
        if (!isset($this->_values[$name])) {
1185
            $relationship = $this->$name();
1186
            $this->_values[$name] = $relationship->getResults();
1187
        }
1188
1189
        return $this->_values[$name];
1190
    }
1191
1192
    /////////////////////////////
1193
    // Events
1194
    /////////////////////////////
1195
1196
    /**
1197
     * Gets the event dispatcher.
1198
     *
1199
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1200
     */
1201
    public static function getDispatcher($ignoreCache = false)
1202
    {
1203
        $class = get_called_class();
1204
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1205
            self::$dispatchers[$class] = new EventDispatcher();
1206
        }
1207
1208
        return self::$dispatchers[$class];
1209
    }
1210
1211
    /**
1212
     * Subscribes to a listener to an event.
1213
     *
1214
     * @param string   $event    event name
1215
     * @param callable $listener
1216
     * @param int      $priority optional priority, higher #s get called first
1217
     */
1218
    public static function listen($event, callable $listener, $priority = 0)
1219
    {
1220
        static::getDispatcher()->addListener($event, $listener, $priority);
1221
    }
1222
1223
    /**
1224
     * Adds a listener to the model.creating event.
1225
     *
1226
     * @param callable $listener
1227
     * @param int      $priority
1228
     */
1229
    public static function creating(callable $listener, $priority = 0)
1230
    {
1231
        static::listen(ModelEvent::CREATING, $listener, $priority);
1232
    }
1233
1234
    /**
1235
     * Adds a listener to the model.created event.
1236
     *
1237
     * @param callable $listener
1238
     * @param int      $priority
1239
     */
1240
    public static function created(callable $listener, $priority = 0)
1241
    {
1242
        static::listen(ModelEvent::CREATED, $listener, $priority);
1243
    }
1244
1245
    /**
1246
     * Adds a listener to the model.updating event.
1247
     *
1248
     * @param callable $listener
1249
     * @param int      $priority
1250
     */
1251
    public static function updating(callable $listener, $priority = 0)
1252
    {
1253
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1254
    }
1255
1256
    /**
1257
     * Adds a listener to the model.updated event.
1258
     *
1259
     * @param callable $listener
1260
     * @param int      $priority
1261
     */
1262
    public static function updated(callable $listener, $priority = 0)
1263
    {
1264
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1265
    }
1266
1267
    /**
1268
     * Adds a listener to the model.deleting event.
1269
     *
1270
     * @param callable $listener
1271
     * @param int      $priority
1272
     */
1273
    public static function deleting(callable $listener, $priority = 0)
1274
    {
1275
        static::listen(ModelEvent::DELETING, $listener, $priority);
1276
    }
1277
1278
    /**
1279
     * Adds a listener to the model.deleted event.
1280
     *
1281
     * @param callable $listener
1282
     * @param int      $priority
1283
     */
1284
    public static function deleted(callable $listener, $priority = 0)
1285
    {
1286
        static::listen(ModelEvent::DELETED, $listener, $priority);
1287
    }
1288
1289
    /**
1290
     * Dispatches an event.
1291
     *
1292
     * @param string $eventName
1293
     *
1294
     * @return ModelEvent
1295
     */
1296
    protected function dispatch($eventName)
1297
    {
1298
        $event = new ModelEvent($this);
1299
1300
        return static::getDispatcher()->dispatch($eventName, $event);
1301
    }
1302
1303
    /////////////////////////////
1304
    // Validation
1305
    /////////////////////////////
1306
1307
    /**
1308
     * Gets the error stack for this model instance. Used to
1309
     * keep track of validation errors.
1310
     *
1311
     * @return Errors
1312
     */
1313
    public function errors()
1314
    {
1315
        if (!$this->_errors) {
1316
            $this->_errors = new Errors($this, self::$locale);
1317
        }
1318
1319
        return $this->_errors;
1320
    }
1321
1322
    /**
1323
     * Checks if the model is valid in its current state.
1324
     *
1325
     * @return bool
1326
     */
1327
    public function valid()
1328
    {
1329
        // clear any previous errors
1330
        $this->errors()->clear();
1331
1332
        // run the validator against the model values
1333
        $validator = $this->getValidator();
1334
        $values = $this->_unsaved + $this->_values;
1335
        $validated = $validator->validate($values);
1336
1337
        // add back any modified unsaved values
1338
        foreach (array_keys($this->_unsaved) as $k) {
1339
            $this->_unsaved[$k] = $values[$k];
1340
        }
1341
1342
        return $validated;
1343
    }
1344
1345
    /**
1346
     * Gets a new validator instance for this model.
1347
     * 
1348
     * @return Validator
1349
     */
1350
    public function getValidator()
1351
    {
1352
        return new Validator(static::$validations, $this->errors());
1353
    }
1354
}
1355