Completed
Push — master ( 3c6b88...473a6c )
by Jared
02:29
created

Model::getPropertyTitle()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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