Completed
Push — master ( ca5244...ff13dd )
by Jared
02:25
created

Model   F

Complexity

Total Complexity 163

Size/Duplication

Total Lines 1355
Duplicated Lines 8.41 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 23
Bugs 0 Features 3
Metric Value
wmc 163
c 23
b 0
f 3
lcom 1
cbo 13
dl 114
loc 1355
rs 2.8148

65 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
B initialize() 0 22 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 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 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
B valid() 0 23 5
C validateValue() 9 36 11
A checkUniqueness() 9 14 3
B hasRequiredValues() 0 18 5
A errors() 0 8 2

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