Completed
Push — master ( 98b7ad...3c6b88 )
by Jared
02:33
created

Model   F

Complexity

Total Complexity 138

Size/Duplication

Total Lines 1268
Duplicated Lines 8.99 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 33
Bugs 3 Features 5
Metric Value
wmc 138
c 33
b 3
f 5
lcom 1
cbo 14
dl 114
loc 1268
rs 1.913

64 Methods

Rating   Name   Duplication   Size   Complexity  
A getNewIds() 0 15 4
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 __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 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() 9 53 10
C set() 9 52 11
B delete() 0 26 5
A persisted() 0 4 1
A refresh() 0 17 3
A refreshWith() 0 8 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 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
        ],
138
        'updated_at' => [
139
            'type' => self::TYPE_DATE,
140
        ],
141
    ];
142
143
    /**
144
     * @staticvar array
145
     */
146
    private static $timestampValidations = [
147
        'created_at' => 'timestamp|db_timestamp',
148
        'updated_at' => 'timestamp|db_timestamp',
149
    ];
150
151
    /**
152
     * @staticvar array
153
     */
154
    private static $initialized = [];
155
156
    /**
157
     * @staticvar DriverInterface
158
     */
159
    private static $driver;
160
161
    /**
162
     * @staticvar Locale
163
     */
164
    private static $locale;
165
166
    /**
167
     * @staticvar array
168
     */
169
    private static $accessors = [];
170
171
    /**
172
     * @staticvar array
173
     */
174
    private static $mutators = [];
175
176
    /**
177
     * @var bool
178
     */
179
    private $_ignoreUnsaved;
180
181
    /**
182
     * Creates a new model object.
183
     *
184
     * @param array $values values to fill model with
185
     */
186
    public function __construct(array $values = [])
187
    {
188
        $this->_values = $values;
189
        $this->app = self::$injectedApp;
190
191
        // ensure the initialize function is called only once
192
        $k = get_called_class();
193
        if (!isset(self::$initialized[$k])) {
194
            $this->initialize();
195
            self::$initialized[$k] = true;
196
        }
197
    }
198
199
    /**
200
     * The initialize() method is called once per model. It's used
201
     * to perform any one-off tasks before the model gets
202
     * constructed. This is a great place to add any model
203
     * properties. When extending this method be sure to call
204
     * parent::initialize() as some important stuff happens here.
205
     * If extending this method to add properties then you should
206
     * call parent::initialize() after adding any properties.
207
     */
208
    protected function initialize()
209
    {
210
        // add in the default ID property
211
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
212
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
213
        }
214
215
        // add in the auto timestamp properties
216
        if (property_exists(get_called_class(), 'autoTimestamps')) {
217
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
218
219
            static::$validations = array_replace(self::$timestampValidations, static::$validations);
220
        }
221
222
        // fill in each property by extending the property
223
        // definition base
224
        foreach (static::$properties as &$property) {
225
            $property = array_replace(self::$propertyDefinitionBase, $property);
226
        }
227
228
        // order the properties array by name for consistency
229
        // since it is constructed in a random order
230
        ksort(static::$properties);
231
    }
232
233
    /**
234
     * Injects a DI container.
235
     *
236
     * @param \Pimple\Container $app
237
     */
238
    public static function inject(Container $app)
239
    {
240
        self::$injectedApp = $app;
241
    }
242
243
    /**
244
     * Gets the DI container used for this model.
245
     *
246
     * @return \Pimple\Container
247
     */
248
    public function getApp()
249
    {
250
        return $this->app;
251
    }
252
253
    /**
254
     * Sets the driver for all models.
255
     *
256
     * @param DriverInterface $driver
257
     */
258
    public static function setDriver(DriverInterface $driver)
259
    {
260
        self::$driver = $driver;
261
    }
262
263
    /**
264
     * Gets the driver for all models.
265
     *
266
     * @return DriverInterface
267
     *
268
     * @throws DriverMissingException
269
     */
270
    public static function getDriver()
271
    {
272
        if (!self::$driver) {
273
            throw new DriverMissingException('A model driver has not been set yet.');
274
        }
275
276
        return self::$driver;
277
    }
278
279
    /**
280
     * Clears the driver for all models.
281
     */
282
    public static function clearDriver()
283
    {
284
        self::$driver = null;
285
    }
286
287
    /**
288
     * Sets the locale instance for all models.
289
     *
290
     * @param Locale $locale
291
     */
292
    public static function setLocale(Locale $locale)
293
    {
294
        self::$locale = $locale;
295
    }
296
297
    /**
298
     * Clears the locale for all models.
299
     */
300
    public static function clearLocale()
301
    {
302
        self::$locale = null;
303
    }
304
305
    /**
306
     * Gets the name of the model without namespacing.
307
     *
308
     * @return string
309
     */
310
    public static function modelName()
311
    {
312
        return explode('\\', get_called_class())[0];
313
    }
314
315
    /**
316
     * Gets the model ID.
317
     *
318
     * @return string|number|null ID
319
     */
320
    public function id()
321
    {
322
        $ids = $this->ids();
323
324
        // if a single ID then return it
325
        if (count($ids) === 1) {
326
            return reset($ids);
327
        }
328
329
        // if multiple IDs then return a comma-separated list
330
        return implode(',', $ids);
331
    }
332
333
    /**
334
     * Gets a key-value map of the model ID.
335
     *
336
     * @return array ID map
337
     */
338
    public function ids()
339
    {
340
        return $this->get(static::$ids);
341
    }
342
343
    /////////////////////////////
344
    // Magic Methods
345
    /////////////////////////////
346
347
    public function __toString()
348
    {
349
        return get_called_class().'('.$this->id().')';
350
    }
351
352
    public function __get($name)
353
    {
354
        return array_values($this->get([$name]))[0];
355
    }
356
357
    public function __set($name, $value)
358
    {
359
        $this->setValue($name, $value);
360
    }
361
362
    public function __isset($name)
363
    {
364
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
365
    }
366
367
    public function __unset($name)
368
    {
369
        if (static::isRelationship($name)) {
370
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
371
        }
372
373
        if (array_key_exists($name, $this->_unsaved)) {
374
            unset($this->_unsaved[$name]);
375
        }
376
    }
377
378
    public static function __callStatic($name, $parameters)
379
    {
380
        // Any calls to unkown static methods should be deferred to
381
        // the query. This allows calls like User::where()
382
        // 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...
383
        return call_user_func_array([static::query(), $name], $parameters);
384
    }
385
386
    /////////////////////////////
387
    // ArrayAccess Interface
388
    /////////////////////////////
389
390
    public function offsetExists($offset)
391
    {
392
        return isset($this->$offset);
393
    }
394
395
    public function offsetGet($offset)
396
    {
397
        return $this->$offset;
398
    }
399
400
    public function offsetSet($offset, $value)
401
    {
402
        $this->$offset = $value;
403
    }
404
405
    public function offsetUnset($offset)
406
    {
407
        unset($this->$offset);
408
    }
409
410
    /////////////////////////////
411
    // Property Definitions
412
    /////////////////////////////
413
414
    /**
415
     * Gets all the property definitions for the model.
416
     *
417
     * @return array key-value map of properties
418
     */
419
    public static function getProperties()
420
    {
421
        return static::$properties;
422
    }
423
424
    /**
425
     * Gets a property defition for the model.
426
     *
427
     * @param string $property property to lookup
428
     *
429
     * @return array|null property
430
     */
431
    public static function getProperty($property)
432
    {
433
        return array_value(static::$properties, $property);
434
    }
435
436
    /**
437
     * Gets the names of the model ID properties.
438
     *
439
     * @return array
440
     */
441
    public static function getIdProperties()
442
    {
443
        return static::$ids;
444
    }
445
446
    /**
447
     * Builds an existing model instance given a single ID value or
448
     * ordered array of ID values.
449
     *
450
     * @param mixed $id
451
     *
452
     * @return Model
453
     */
454
    public static function buildFromId($id)
455
    {
456
        $ids = [];
457
        $id = (array) $id;
458
        foreach (static::$ids as $j => $k) {
459
            $ids[$k] = $id[$j];
460
        }
461
462
        $model = new static($ids);
463
464
        return $model;
465
    }
466
467
    /**
468
     * Checks if the model has a property.
469
     *
470
     * @param string $property property
471
     *
472
     * @return bool has property
473
     */
474
    public static function hasProperty($property)
475
    {
476
        return isset(static::$properties[$property]);
477
    }
478
479
    /**
480
     * Gets the mutator method name for a given proeprty name.
481
     * Looks for methods in the form of `setPropertyValue`.
482
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
483
     *
484
     * @param string $property property
485
     *
486
     * @return string|false method name if it exists
487
     */
488 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...
489
    {
490
        $class = get_called_class();
491
492
        $k = $class.':'.$property;
493
        if (!array_key_exists($k, self::$mutators)) {
494
            $inflector = Inflector::get();
495
            $method = 'set'.$inflector->camelize($property).'Value';
496
497
            if (!method_exists($class, $method)) {
498
                $method = false;
499
            }
500
501
            self::$mutators[$k] = $method;
502
        }
503
504
        return self::$mutators[$k];
505
    }
506
507
    /**
508
     * Gets the accessor method name for a given proeprty name.
509
     * Looks for methods in the form of `getPropertyValue`.
510
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
511
     *
512
     * @param string $property property
513
     *
514
     * @return string|false method name if it exists
515
     */
516 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...
517
    {
518
        $class = get_called_class();
519
520
        $k = $class.':'.$property;
521
        if (!array_key_exists($k, self::$accessors)) {
522
            $inflector = Inflector::get();
523
            $method = 'get'.$inflector->camelize($property).'Value';
524
525
            if (!method_exists($class, $method)) {
526
                $method = false;
527
            }
528
529
            self::$accessors[$k] = $method;
530
        }
531
532
        return self::$accessors[$k];
533
    }
534
535
    /**
536
     * Checks if a given property is a relationship.
537
     *
538
     * @param string $property
539
     *
540
     * @return bool
541
     */
542
    public static function isRelationship($property)
543
    {
544
        return in_array($property, static::$relationships);
545
    }
546
547
    /**
548
     * Gets the title of a property.
549
     *
550
     * @param string $name
551
     *
552
     * @return string
553
     */
554
    public static function getPropertyTitle($name)
555
    {
556
        // attmept to fetch the title from the Locale service
557
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
558
        if (self::$locale && $title = self::$locale->t($k)) {
559
            if ($title != $k) {
560
                return $title;
561
            }
562
        }
563
564
        return Inflector::get()->humanize($name);
565
    }
566
567
    /////////////////////////////
568
    // Values
569
    /////////////////////////////
570
571
    /**
572
     * Sets an unsaved value.
573
     *
574
     * @param string $name
575
     * @param mixed  $value
576
     *
577
     * @throws BadMethodCallException when setting a relationship
578
     */
579
    public function setValue($name, $value)
580
    {
581
        if (static::isRelationship($name)) {
582
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
583
        }
584
585
        // set using any mutators
586
        if ($mutator = self::getMutator($name)) {
587
            $this->_unsaved[$name] = $this->$mutator($value);
588
        } else {
589
            $this->_unsaved[$name] = $value;
590
        }
591
592
        return $this;
593
    }
594
595
    /**
596
     * Ignores unsaved values when fetching the next value.
597
     *
598
     * @return self
599
     */
600
    public function ignoreUnsaved()
601
    {
602
        $this->_ignoreUnsaved = true;
603
604
        return $this;
605
    }
606
607
    /**
608
     * Gets property values from the model.
609
     *
610
     * This method looks up values from these locations in this
611
     * precedence order (least important to most important):
612
     *  1. local values
613
     *  2. unsaved values
614
     *
615
     * @param array $properties list of property names to fetch values of
616
     *
617
     * @return array
618
     *
619
     * @throws InvalidArgumentException when a property was requested not present in the values
620
     */
621
    public function get(array $properties)
622
    {
623
        // load the values from the local model cache
624
        $values = $this->_values;
625
626
        // unless specified, use any unsaved values
627
        $ignoreUnsaved = $this->_ignoreUnsaved;
628
        $this->_ignoreUnsaved = false;
629
        if (!$ignoreUnsaved) {
630
            $values = array_replace($values, $this->_unsaved);
631
        }
632
633
        // build the response
634
        $result = [];
635
        foreach ($properties as $k) {
636
            $accessor = self::getAccessor($k);
637
638
            // use the supplied value if it's available
639
            if (array_key_exists($k, $values)) {
640
                $result[$k] = $values[$k];
641
            // get relationship values
642
            } elseif (static::isRelationship($k)) {
643
                $result[$k] = $this->loadRelationship($k);
644
            // set any missing values to null
645
            } elseif ($property = static::getProperty($k)) {
0 ignored issues
show
Unused Code introduced by
$property is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
646
                $result[$k] = $this->_values[$k] = null;
647
            // throw an exception for non-properties that do not
648
            // have an accessor
649
            } 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...
650
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
651
            // otherwise the value is considered null
652
            } else {
653
                $result[$k] = null;
654
            }
655
656
            // call any accessors
657
            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...
658
                $result[$k] = $this->$accessor($result[$k]);
659
            }
660
        }
661
662
        return $result;
663
    }
664
665
    /**
666
     * Converts the model to an array.
667
     *
668
     * @return array model array
669
     */
670
    public function toArray()
671
    {
672
        // build the list of properties to retrieve
673
        $properties = array_keys(static::$properties);
674
675
        // remove any hidden properties
676
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
677
        $properties = array_diff($properties, $hide);
678
679
        // add any appended properties
680
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
681
        $properties = array_merge($properties, $append);
682
683
        // get the values for the properties
684
        $result = $this->get($properties);
685
686
        // convert any models to arrays
687
        foreach ($result as &$value) {
688
            if ($value instanceof self) {
689
                $value = $value->toArray();
690
            }
691
        }
692
693
        return $result;
694
    }
695
696
    /////////////////////////////
697
    // Persistence
698
    /////////////////////////////
699
700
    /**
701
     * Saves the model.
702
     *
703
     * @return bool
704
     */
705
    public function save()
706
    {
707
        if (!$this->_persisted) {
708
            return $this->create();
709
        }
710
711
        return $this->set($this->_unsaved);
712
    }
713
714
    /**
715
     * Creates a new model.
716
     *
717
     * @param array $data optional key-value properties to set
718
     *
719
     * @return bool
720
     *
721
     * @throws BadMethodCallException when called on an existing model
722
     */
723
    public function create(array $data = [])
724
    {
725
        if ($this->_persisted) {
726
            throw new BadMethodCallException('Cannot call create() on an existing model');
727
        }
728
729
        if (!empty($data)) {
730
            foreach ($data as $k => $value) {
731
                $this->setValue($k, $value);
732
            }
733
        }
734
735
        // add in any preset values
736
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
737
738
        // dispatch the model.creating event
739
        $event = $this->dispatch(ModelEvent::CREATING);
740
        if ($event->isPropagationStopped()) {
741
            return false;
742
        }
743
744
        // validate the model
745
        if (!$this->valid()) {
746
            return false;
747
        }
748
749
        // build the insert array
750
        $insertValues = [];
751 View Code Duplication
        foreach ($this->_unsaved as $k => $value) {
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...
752
            // remove any non-existent or immutable properties
753
            $property = static::getProperty($k);
754
            if ($property === null || $property['mutable'] == self::IMMUTABLE) {
755
                continue;
756
            }
757
758
            $insertValues[$k] = $value;
759
        }
760
761
        if (!self::getDriver()->createModel($this, $insertValues)) {
762
            return false;
763
        }
764
765
        // update the model with the persisted values and new ID(s)
766
        $newValues = array_replace(
767
            $insertValues,
768
            $this->getNewIds());
769
        $this->refreshWith($newValues);
770
771
        // dispatch the model.created event
772
        $event = $this->dispatch(ModelEvent::CREATED);
773
774
        return !$event->isPropagationStopped();
775
    }
776
777
    /**
778
     * Gets the IDs for a newly created model.
779
     *
780
     * @return string
781
     */
782
    protected function getNewIds()
783
    {
784
        $ids = [];
785
        foreach (static::$ids as $k) {
786
            // attempt use the supplied value if the ID property is mutable
787
            $property = static::getProperty($k);
788
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
789
                $ids[$k] = $this->_unsaved[$k];
790
            } else {
791
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
792
            }
793
        }
794
795
        return $ids;
796
    }
797
798
    /**
799
     * Updates the model.
800
     *
801
     * @param array $data optional key-value properties to set
802
     *
803
     * @return bool
804
     *
805
     * @throws BadMethodCallException when not called on an existing model
806
     */
807
    public function set(array $data = [])
808
    {
809
        if (!$this->_persisted) {
810
            throw new BadMethodCallException('Can only call set() on an existing model');
811
        }
812
813
        if (!empty($data)) {
814
            foreach ($data as $k => $value) {
815
                $this->setValue($k, $value);
816
            }
817
        }
818
819
        // not updating anything?
820
        if (count($this->_unsaved) === 0) {
821
            return true;
822
        }
823
824
        // dispatch the model.updating event
825
        $event = $this->dispatch(ModelEvent::UPDATING);
826
        if ($event->isPropagationStopped()) {
827
            return false;
828
        }
829
830
        // validate the model
831
        if (!$this->valid()) {
832
            return false;
833
        }
834
835
        // build the update array
836
        $updateValues = [];
837 View Code Duplication
        foreach ($this->_unsaved as $k => $value) {
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...
838
            // remove any non-existent or immutable properties
839
            $property = static::getProperty($k);
840
            if ($property === null || $property['mutable'] != self::MUTABLE) {
841
                continue;
842
            }
843
844
            $updateValues[$k] = $value;
845
        }
846
847
        if (!self::getDriver()->updateModel($this, $updateValues)) {
848
            return false;
849
        }
850
851
        // update the model with the persisted values
852
        $this->refreshWith($updateValues);
853
854
        // dispatch the model.updated event
855
        $event = $this->dispatch(ModelEvent::UPDATED);
856
857
        return !$event->isPropagationStopped();
858
    }
859
860
    /**
861
     * Delete the model.
862
     *
863
     * @return bool success
864
     */
865
    public function delete()
866
    {
867
        if (!$this->_persisted) {
868
            throw new BadMethodCallException('Can only call delete() on an existing model');
869
        }
870
871
        // dispatch the model.deleting event
872
        $event = $this->dispatch(ModelEvent::DELETING);
873
        if ($event->isPropagationStopped()) {
874
            return false;
875
        }
876
877
        $deleted = self::getDriver()->deleteModel($this);
878
879
        if ($deleted) {
880
            // dispatch the model.deleted event
881
            $event = $this->dispatch(ModelEvent::DELETED);
882
            if ($event->isPropagationStopped()) {
883
                return false;
884
            }
885
886
            $this->_persisted = false;
887
        }
888
889
        return $deleted;
890
    }
891
892
    /**
893
     * Tells if the model has been persisted.
894
     *
895
     * @return bool
896
     */
897
    public function persisted()
898
    {
899
        return $this->_persisted;
900
    }
901
902
    /**
903
     * Loads the model from the data layer.
904
     *
905
     * @return self
906
     *
907
     * @throws NotFoundException
908
     */
909
    public function refresh()
910
    {
911
        if (!$this->_persisted) {
912
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
913
        }
914
915
        $query = static::query();
916
        $query->where($this->ids());
917
918
        $values = self::getDriver()->queryModels($query);
919
920
        if (count($values) === 0) {
921
            return $this;
922
        }
923
924
        return $this->refreshWith($values[0]);
925
    }
926
927
    /**
928
     * Loads values into the model retrieved from the data layer.
929
     *
930
     * @param array $values values
931
     *
932
     * @return self
933
     */
934
    public function refreshWith(array $values)
935
    {
936
        $this->_persisted = true;
937
        $this->_values = $values;
938
        $this->_unsaved = [];
939
940
        return $this;
941
    }
942
943
    /////////////////////////////
944
    // Queries
945
    /////////////////////////////
946
947
    /**
948
     * Generates a new query instance.
949
     *
950
     * @return Query
951
     */
952
    public static function query()
953
    {
954
        // Create a new model instance for the query to ensure
955
        // that the model's initialize() method gets called.
956
        // Otherwise, the property definitions will be incomplete.
957
        $model = new static();
958
959
        return new Query($model);
960
    }
961
962
    /**
963
     * Finds a single instance of a model given it's ID.
964
     *
965
     * @param mixed $id
966
     *
967
     * @return Model|null
968
     */
969
    public static function find($id)
970
    {
971
        $model = static::buildFromId($id);
972
973
        return static::query()->where($model->ids())->first();
974
    }
975
976
    /**
977
     * Finds a single instance of a model given it's ID or throws an exception.
978
     *
979
     * @param mixed $id
980
     *
981
     * @return Model|false
982
     *
983
     * @throws NotFoundException when a model could not be found
984
     */
985
    public static function findOrFail($id)
986
    {
987
        $model = static::find($id);
988
        if (!$model) {
989
            throw new NotFoundException('Could not find the requested '.static::modelName());
990
        }
991
992
        return $model;
993
    }
994
995
    /**
996
     * Gets the toal number of records matching an optional criteria.
997
     *
998
     * @param array $where criteria
999
     *
1000
     * @return int total
1001
     */
1002
    public static function totalRecords(array $where = [])
1003
    {
1004
        $query = static::query();
1005
        $query->where($where);
1006
1007
        return self::getDriver()->totalRecords($query);
1008
    }
1009
1010
    /////////////////////////////
1011
    // Relationships
1012
    /////////////////////////////
1013
1014
    /**
1015
     * Creates the parent side of a One-To-One relationship.
1016
     *
1017
     * @param string $model      foreign model class
1018
     * @param string $foreignKey identifying key on foreign model
1019
     * @param string $localKey   identifying key on local model
1020
     *
1021
     * @return \Pulsar\Relation\Relation
1022
     */
1023 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...
1024
    {
1025
        // the default local key would look like `user_id`
1026
        // for a model named User
1027
        if (!$foreignKey) {
1028
            $inflector = Inflector::get();
1029
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1030
        }
1031
1032
        if (!$localKey) {
1033
            $localKey = self::DEFAULT_ID_PROPERTY;
1034
        }
1035
1036
        return new HasOne($model, $foreignKey, $localKey, $this);
1037
    }
1038
1039
    /**
1040
     * Creates the child side of a One-To-One or One-To-Many relationship.
1041
     *
1042
     * @param string $model      foreign model class
1043
     * @param string $foreignKey identifying key on foreign model
1044
     * @param string $localKey   identifying key on local model
1045
     *
1046
     * @return \Pulsar\Relation\Relation
1047
     */
1048 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...
1049
    {
1050
        if (!$foreignKey) {
1051
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1052
        }
1053
1054
        // the default local key would look like `user_id`
1055
        // for a model named User
1056
        if (!$localKey) {
1057
            $inflector = Inflector::get();
1058
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1059
        }
1060
1061
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1062
    }
1063
1064
    /**
1065
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1066
     *
1067
     * @param string $model      foreign model class
1068
     * @param string $foreignKey identifying key on foreign model
1069
     * @param string $localKey   identifying key on local model
1070
     *
1071
     * @return \Pulsar\Relation\Relation
1072
     */
1073 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...
1074
    {
1075
        // the default local key would look like `user_id`
1076
        // for a model named User
1077
        if (!$foreignKey) {
1078
            $inflector = Inflector::get();
1079
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1080
        }
1081
1082
        if (!$localKey) {
1083
            $localKey = self::DEFAULT_ID_PROPERTY;
1084
        }
1085
1086
        return new HasMany($model, $foreignKey, $localKey, $this);
1087
    }
1088
1089
    /**
1090
     * Creates the child side of a Many-To-Many relationship.
1091
     *
1092
     * @param string $model      foreign model class
1093
     * @param string $foreignKey identifying key on foreign model
1094
     * @param string $localKey   identifying key on local model
1095
     *
1096
     * @return \Pulsar\Relation\Relation
1097
     */
1098 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...
1099
    {
1100
        if (!$foreignKey) {
1101
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1102
        }
1103
1104
        // the default local key would look like `user_id`
1105
        // for a model named User
1106
        if (!$localKey) {
1107
            $inflector = Inflector::get();
1108
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1109
        }
1110
1111
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1112
    }
1113
1114
    /**
1115
     * Loads a given relationship (if not already) and returns
1116
     * its results.
1117
     *
1118
     * @param string $name
1119
     *
1120
     * @return mixed
1121
     */
1122
    protected function loadRelationship($name)
1123
    {
1124
        if (!isset($this->_values[$name])) {
1125
            $relationship = $this->$name();
1126
            $this->_values[$name] = $relationship->getResults();
1127
        }
1128
1129
        return $this->_values[$name];
1130
    }
1131
1132
    /////////////////////////////
1133
    // Events
1134
    /////////////////////////////
1135
1136
    /**
1137
     * Gets the event dispatcher.
1138
     *
1139
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1140
     */
1141
    public static function getDispatcher($ignoreCache = false)
1142
    {
1143
        $class = get_called_class();
1144
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1145
            self::$dispatchers[$class] = new EventDispatcher();
1146
        }
1147
1148
        return self::$dispatchers[$class];
1149
    }
1150
1151
    /**
1152
     * Subscribes to a listener to an event.
1153
     *
1154
     * @param string   $event    event name
1155
     * @param callable $listener
1156
     * @param int      $priority optional priority, higher #s get called first
1157
     */
1158
    public static function listen($event, callable $listener, $priority = 0)
1159
    {
1160
        static::getDispatcher()->addListener($event, $listener, $priority);
1161
    }
1162
1163
    /**
1164
     * Adds a listener to the model.creating event.
1165
     *
1166
     * @param callable $listener
1167
     * @param int      $priority
1168
     */
1169
    public static function creating(callable $listener, $priority = 0)
1170
    {
1171
        static::listen(ModelEvent::CREATING, $listener, $priority);
1172
    }
1173
1174
    /**
1175
     * Adds a listener to the model.created event.
1176
     *
1177
     * @param callable $listener
1178
     * @param int      $priority
1179
     */
1180
    public static function created(callable $listener, $priority = 0)
1181
    {
1182
        static::listen(ModelEvent::CREATED, $listener, $priority);
1183
    }
1184
1185
    /**
1186
     * Adds a listener to the model.updating event.
1187
     *
1188
     * @param callable $listener
1189
     * @param int      $priority
1190
     */
1191
    public static function updating(callable $listener, $priority = 0)
1192
    {
1193
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1194
    }
1195
1196
    /**
1197
     * Adds a listener to the model.updated event.
1198
     *
1199
     * @param callable $listener
1200
     * @param int      $priority
1201
     */
1202
    public static function updated(callable $listener, $priority = 0)
1203
    {
1204
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1205
    }
1206
1207
    /**
1208
     * Adds a listener to the model.deleting event.
1209
     *
1210
     * @param callable $listener
1211
     * @param int      $priority
1212
     */
1213
    public static function deleting(callable $listener, $priority = 0)
1214
    {
1215
        static::listen(ModelEvent::DELETING, $listener, $priority);
1216
    }
1217
1218
    /**
1219
     * Adds a listener to the model.deleted event.
1220
     *
1221
     * @param callable $listener
1222
     * @param int      $priority
1223
     */
1224
    public static function deleted(callable $listener, $priority = 0)
1225
    {
1226
        static::listen(ModelEvent::DELETED, $listener, $priority);
1227
    }
1228
1229
    /**
1230
     * Dispatches an event.
1231
     *
1232
     * @param string $eventName
1233
     *
1234
     * @return ModelEvent
1235
     */
1236
    protected function dispatch($eventName)
1237
    {
1238
        $event = new ModelEvent($this);
1239
1240
        return static::getDispatcher()->dispatch($eventName, $event);
1241
    }
1242
1243
    /////////////////////////////
1244
    // Validation
1245
    /////////////////////////////
1246
1247
    /**
1248
     * Gets the error stack for this model instance. Used to
1249
     * keep track of validation errors.
1250
     *
1251
     * @return Errors
1252
     */
1253
    public function errors()
1254
    {
1255
        if (!$this->_errors) {
1256
            $this->_errors = new Errors($this, self::$locale);
1257
        }
1258
1259
        return $this->_errors;
1260
    }
1261
1262
    /**
1263
     * Checks if the model is valid in its current state.
1264
     *
1265
     * @return bool
1266
     */
1267
    public function valid()
1268
    {
1269
        // clear any previous errors
1270
        $this->errors()->clear();
1271
1272
        // run the validator against the model values
1273
        $validator = $this->getValidator();
1274
        $values = $this->_values + $this->_unsaved;
1275
        $validated = $validator->validate($values);
1276
1277
        // add back any modified unsaved values
1278
        foreach (array_keys($this->_unsaved) as $k) {
1279
            $this->_unsaved[$k] = $values[$k];
1280
        }
1281
1282
        return $validated;
1283
    }
1284
1285
    /**
1286
     * Gets a new validator instance for this model.
1287
     * 
1288
     * @return Validator
1289
     */
1290
    public function getValidator()
1291
    {
1292
        return new Validator(static::$validations, $this->errors());
1293
    }
1294
}
1295