Completed
Push — master ( a9e5d9...4a2573 )
by Jared
02:12
created

Model   F

Complexity

Total Complexity 148

Size/Duplication

Total Lines 1314
Duplicated Lines 7.31 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 29
Bugs 2 Features 5
Metric Value
wmc 148
c 29
b 2
f 5
lcom 1
cbo 14
dl 96
loc 1314
rs 1.6296

66 Methods

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