Completed
Push — master ( f21d6d...4505eb )
by Jared
03:02
created

Model::creating()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 2
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\MassAssignmentException;
20
use Pulsar\Exception\NotFoundException;
21
use Pulsar\Relation\HasOne;
22
use Pulsar\Relation\BelongsTo;
23
use Pulsar\Relation\HasMany;
24
use Pulsar\Relation\BelongsToMany;
25
use Pimple\Container;
26
use Symfony\Component\EventDispatcher\EventDispatcher;
27
28
abstract class Model implements \ArrayAccess
29
{
30
    const TYPE_STRING = 'string';
31
    const TYPE_INTEGER = 'integer';
32
    const TYPE_FLOAT = 'float';
33
    const TYPE_BOOLEAN = 'boolean';
34
    const TYPE_DATE = 'date';
35
    const TYPE_OBJECT = 'object';
36
    const TYPE_ARRAY = 'array';
37
38
    const DEFAULT_ID_PROPERTY = 'id';
39
40
    /////////////////////////////
41
    // Model visible variables
42
    /////////////////////////////
43
44
    /**
45
     * List of model ID property names.
46
     *
47
     * @staticvar array
48
     */
49
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
50
51
    /**
52
     * Validation rules expressed as a key-value map with
53
     * property names as the keys.
54
     * i.e. ['name' => 'string:2'].
55
     *
56
     * @staticvar array
57
     */
58
    protected static $validations = [];
59
60
    /**
61
     * @staticvar array
62
     */
63
    protected static $relationships = [];
64
65
    /**
66
     * @staticvar \Pimple\Container
67
     */
68
    protected static $injectedApp;
69
70
    /**
71
     * @staticvar array
72
     */
73
    protected static $dispatchers;
74
75
    /**
76
     * @var \Pimple\Container
77
     */
78
    protected $app;
79
80
    /**
81
     * @var array
82
     */
83
    protected $_values = [];
84
85
    /**
86
     * @var array
87
     */
88
    protected $_unsaved = [];
89
90
    /**
91
     * @var bool
92
     */
93
    protected $_persisted = false;
94
95
    /**
96
     * @var Errors
97
     */
98
    protected $_errors;
99
100
    /////////////////////////////
101
    // Base model variables
102
    /////////////////////////////
103
104
    /**
105
     * @staticvar array
106
     */
107
    private static $initialized = [];
108
109
    /**
110
     * @staticvar DriverInterface
111
     */
112
    private static $driver;
113
114
    /**
115
     * @staticvar Locale
116
     */
117
    private static $locale;
118
119
    /**
120
     * @staticvar array
121
     */
122
    private static $accessors = [];
123
124
    /**
125
     * @staticvar array
126
     */
127
    private static $mutators = [];
128
129
    /**
130
     * @var bool
131
     */
132
    private $_ignoreUnsaved;
133
134
    /**
135
     * Creates a new model object.
136
     *
137
     * @param array $values values to fill model with
138
     */
139
    public function __construct(array $values = [])
140
    {
141
        $this->_values = $values;
142
        $this->app = self::$injectedApp;
143
144
        // ensure the initialize function is called only once
145
        $k = get_called_class();
146
        if (!isset(self::$initialized[$k])) {
147
            $this->initialize();
148
            self::$initialized[$k] = true;
149
        }
150
    }
151
152
    /**
153
     * The initialize() method is called once per model. It's used
154
     * to perform any one-off tasks before the model gets
155
     * constructed. This is a great place to add any model
156
     * properties. When extending this method be sure to call
157
     * parent::initialize() as some important stuff happens here.
158
     * If extending this method to add properties then you should
159
     * call parent::initialize() after adding any properties.
160
     */
161
    protected function initialize()
162
    {
163
        // add in the default ID property
164
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
165
            if (property_exists($this, 'casts') && !isset(static::$casts[self::DEFAULT_ID_PROPERTY])) {
166
                static::$casts[self::DEFAULT_ID_PROPERTY] = self::TYPE_INTEGER;
167
            }
168
        }
169
170
        // generates created_at and updated_at timestamps
171
        if (property_exists($this, 'autoTimestamps')) {
172
            $this->installAutoTimestamps();
173
        }
174
    }
175
176
    private function installAutoTimestamps()
177
    {
178
        if (property_exists($this, 'casts')) {
179
            static::$casts['created_at'] = self::TYPE_DATE;
180
            static::$casts['updated_at'] = self::TYPE_DATE;
181
        }
182
183
        self::creating(function (ModelEvent $event) {
184
            $model = $event->getModel();
185
            $model->created_at = time();
0 ignored issues
show
Documentation introduced by
The property created_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
186
            $model->updated_at = time();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
187
        });
188
189
        self::updating(function (ModelEvent $event) {
190
            $event->getModel()->updated_at = time();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
191
        });
192
    }
193
194
    /**
195
     * Injects a DI container.
196
     *
197
     * @param \Pimple\Container $app
198
     */
199
    public static function inject(Container $app)
200
    {
201
        self::$injectedApp = $app;
202
    }
203
204
    /**
205
     * Gets the DI container used for this model.
206
     *
207
     * @return \Pimple\Container
208
     */
209
    public function getApp()
210
    {
211
        return $this->app;
212
    }
213
214
    /**
215
     * Sets the driver for all models.
216
     *
217
     * @param DriverInterface $driver
218
     */
219
    public static function setDriver(DriverInterface $driver)
220
    {
221
        self::$driver = $driver;
222
    }
223
224
    /**
225
     * Gets the driver for all models.
226
     *
227
     * @return DriverInterface
228
     *
229
     * @throws DriverMissingException
230
     */
231
    public static function getDriver()
232
    {
233
        if (!self::$driver) {
234
            throw new DriverMissingException('A model driver has not been set yet.');
235
        }
236
237
        return self::$driver;
238
    }
239
240
    /**
241
     * Clears the driver for all models.
242
     */
243
    public static function clearDriver()
244
    {
245
        self::$driver = null;
246
    }
247
248
    /**
249
     * Sets the locale instance for all models.
250
     *
251
     * @param Locale $locale
252
     */
253
    public static function setLocale(Locale $locale)
254
    {
255
        self::$locale = $locale;
256
    }
257
258
    /**
259
     * Clears the locale for all models.
260
     */
261
    public static function clearLocale()
262
    {
263
        self::$locale = null;
264
    }
265
266
    /**
267
     * Gets the name of the model without namespacing.
268
     *
269
     * @return string
270
     */
271
    public static function modelName()
272
    {
273
        return explode('\\', get_called_class())[0];
274
    }
275
276
    /**
277
     * Gets the model ID.
278
     *
279
     * @return string|number|null ID
280
     */
281
    public function id()
282
    {
283
        $ids = $this->ids();
284
285
        // if a single ID then return it
286
        if (count($ids) === 1) {
287
            return reset($ids);
288
        }
289
290
        // if multiple IDs then return a comma-separated list
291
        return implode(',', $ids);
292
    }
293
294
    /**
295
     * Gets a key-value map of the model ID.
296
     *
297
     * @return array ID map
298
     */
299
    public function ids()
300
    {
301
        return $this->get(static::$ids);
302
    }
303
304
    /////////////////////////////
305
    // Magic Methods
306
    /////////////////////////////
307
308
    public function __toString()
309
    {
310
        return get_called_class().'('.$this->id().')';
311
    }
312
313
    public function __get($name)
314
    {
315
        return array_values($this->get([$name]))[0];
316
    }
317
318
    public function __set($name, $value)
319
    {
320
        $this->setValue($name, $value);
321
    }
322
323
    public function __isset($name)
324
    {
325
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
326
    }
327
328
    public function __unset($name)
329
    {
330
        if (static::isRelationship($name)) {
331
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
332
        }
333
334
        if (array_key_exists($name, $this->_unsaved)) {
335
            unset($this->_unsaved[$name]);
336
        }
337
    }
338
339
    public static function __callStatic($name, $parameters)
340
    {
341
        // Any calls to unkown static methods should be deferred to
342
        // the query. This allows calls like User::where()
343
        // 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...
344
        return call_user_func_array([static::query(), $name], $parameters);
345
    }
346
347
    /////////////////////////////
348
    // ArrayAccess Interface
349
    /////////////////////////////
350
351
    public function offsetExists($offset)
352
    {
353
        return isset($this->$offset);
354
    }
355
356
    public function offsetGet($offset)
357
    {
358
        return $this->$offset;
359
    }
360
361
    public function offsetSet($offset, $value)
362
    {
363
        $this->$offset = $value;
364
    }
365
366
    public function offsetUnset($offset)
367
    {
368
        unset($this->$offset);
369
    }
370
371
    /////////////////////////////
372
    // Property Definitions
373
    /////////////////////////////
374
375
    /**
376
     * Gets the names of the model ID properties.
377
     *
378
     * @return array
379
     */
380
    public static function getIdProperties()
381
    {
382
        return static::$ids;
383
    }
384
385
    /**
386
     * Builds an existing model instance given a single ID value or
387
     * ordered array of ID values.
388
     *
389
     * @param mixed $id
390
     *
391
     * @return Model
392
     */
393
    public static function buildFromId($id)
394
    {
395
        $ids = [];
396
        $id = (array) $id;
397
        foreach (static::$ids as $j => $k) {
398
            $ids[$k] = $id[$j];
399
        }
400
401
        $model = new static($ids);
402
403
        return $model;
404
    }
405
406
    /**
407
     * Gets the mutator method name for a given proeprty name.
408
     * Looks for methods in the form of `setPropertyValue`.
409
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
410
     *
411
     * @param string $property
412
     *
413
     * @return string|false method name if it exists
414
     */
415 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...
416
    {
417
        $class = get_called_class();
418
419
        $k = $class.':'.$property;
420
        if (!array_key_exists($k, self::$mutators)) {
421
            $inflector = Inflector::get();
422
            $method = 'set'.$inflector->camelize($property).'Value';
423
424
            if (!method_exists($class, $method)) {
425
                $method = false;
426
            }
427
428
            self::$mutators[$k] = $method;
429
        }
430
431
        return self::$mutators[$k];
432
    }
433
434
    /**
435
     * Gets the accessor method name for a given proeprty name.
436
     * Looks for methods in the form of `getPropertyValue`.
437
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
438
     *
439
     * @param string $property
440
     *
441
     * @return string|false method name if it exists
442
     */
443 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...
444
    {
445
        $class = get_called_class();
446
447
        $k = $class.':'.$property;
448
        if (!array_key_exists($k, self::$accessors)) {
449
            $inflector = Inflector::get();
450
            $method = 'get'.$inflector->camelize($property).'Value';
451
452
            if (!method_exists($class, $method)) {
453
                $method = false;
454
            }
455
456
            self::$accessors[$k] = $method;
457
        }
458
459
        return self::$accessors[$k];
460
    }
461
462
    /**
463
     * Checks if a given property is a relationship.
464
     *
465
     * @param string $property
466
     *
467
     * @return bool
468
     */
469
    public static function isRelationship($property)
470
    {
471
        return in_array($property, static::$relationships);
472
    }
473
474
    /**
475
     * Gets the title of a property.
476
     *
477
     * @param string $name
478
     *
479
     * @return string
480
     */
481
    public static function getPropertyTitle($name)
482
    {
483
        // attmept to fetch the title from the Locale service
484
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
485
        if (self::$locale && $title = self::$locale->t($k)) {
486
            if ($title != $k) {
487
                return $title;
488
            }
489
        }
490
491
        return Inflector::get()->humanize($name);
492
    }
493
494
    /**
495
     * Gets the type cast for a property.
496
     *
497
     * @param string $property
498
     *
499
     * @return string|null
500
     */
501
    public static function getPropertyType($property)
502
    {
503
        if (property_exists(get_called_class(), 'casts')) {
504
            return array_value(static::$casts, $property);
505
        }
506
    }
507
508
    /**
509
     * Casts a value to a given type.
510
     *
511
     * @param string|null $type
512
     * @param mixed       $value
513
     *
514
     * @return mixed casted value
515
     */
516
    public static function cast($type, $value)
517
    {
518
        if ($value === null) {
519
            return;
520
        }
521
522
        switch ($type) {
523
        case self::TYPE_STRING:
524
            return (string) $value;
525
526
        case self::TYPE_INTEGER:
527
            return (int) $value;
528
529
        case self::TYPE_FLOAT:
530
            return (float) $value;
531
532
        case self::TYPE_BOOLEAN:
533
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
534
535
        case self::TYPE_DATE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
536
            // cast dates as unix timestamps
537
            if (!is_numeric($value)) {
538
                return strtotime($value);
539
            } else {
540
                return $value + 0;
541
            }
542
543 View Code Duplication
        case self::TYPE_ARRAY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
544
            // decode JSON into an array
545
            if (is_string($value)) {
546
                return json_decode($value, true);
547
            } else {
548
                return (array) $value;
549
            }
550
551 View Code Duplication
        case self::TYPE_OBJECT:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
552
            // decode JSON into an object
553
            if (is_string($value)) {
554
                return (object) json_decode($value);
555
            } else {
556
                return (object) $value;
557
            }
558
559
        default:
560
            return $value;
561
        }
562
    }
563
564
    /**
565
     * Gets the properties of this model.
566
     *
567
     * @return array
568
     */
569
    public function getProperties()
570
    {
571
        return array_unique(array_merge(
572
            static::$ids, array_keys($this->_values)));
573
    }
574
575
    /**
576
     * Checks if the model has a property.
577
     *
578
     * @param string $property
579
     *
580
     * @return bool has property
581
     */
582
    public function hasProperty($property)
583
    {
584
        return array_key_exists($property, $this->_values) ||
585
               in_array($property, static::$ids);
586
    }
587
588
    /////////////////////////////
589
    // Values
590
    /////////////////////////////
591
592
    /**
593
     * Sets an unsaved value.
594
     *
595
     * @param string $name
596
     * @param mixed  $value
597
     *
598
     * @throws BadMethodCallException when setting a relationship
599
     *
600
     * @return self
601
     */
602
    public function setValue($name, $value)
603
    {
604
        if (static::isRelationship($name)) {
605
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
606
        }
607
608
        // cast the value
609
        if ($type = static::getPropertyType($name)) {
610
            $value = static::cast($type, $value);
611
        }
612
613
        // set using any mutators
614
        if ($mutator = self::getMutator($name)) {
615
            $this->_unsaved[$name] = $this->$mutator($value);
616
        } else {
617
            $this->_unsaved[$name] = $value;
618
        }
619
620
        return $this;
621
    }
622
623
    /**
624
     * Sets a collection values on the model from an untrusted input.
625
     *
626
     * @param array $values
627
     *
628
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
629
     *
630
     * @return self
631
     */
632
    public function setValues($values)
633
    {
634
        // check if the model has a mass assignment whitelist
635
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
636
637
        // if no whitelist, then check for a blacklist
638
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
639
640
        foreach ($values as $k => $value) {
641
            // check for mass assignment violations
642
            if (($permitted && !in_array($k, $permitted)) ||
643
                ($protected && in_array($k, $protected))) {
644
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
645
            }
646
647
            $this->setValue($k, $value);
648
        }
649
650
        return $this;
651
    }
652
653
    /**
654
     * Ignores unsaved values when fetching the next value.
655
     *
656
     * @return self
657
     */
658
    public function ignoreUnsaved()
659
    {
660
        $this->_ignoreUnsaved = true;
661
662
        return $this;
663
    }
664
665
    /**
666
     * Gets property values from the model.
667
     *
668
     * This method looks up values from these locations in this
669
     * precedence order (least important to most important):
670
     *  1. local values
671
     *  2. unsaved values
672
     *
673
     * @param array $properties list of property names to fetch values of
674
     *
675
     * @return array
676
     *
677
     * @throws InvalidArgumentException when a property was requested not present in the values
678
     */
679
    public function get(array $properties)
680
    {
681
        // load the values from the local model cache
682
        $values = $this->_values;
683
684
        // unless specified, use any unsaved values
685
        $ignoreUnsaved = $this->_ignoreUnsaved;
686
        $this->_ignoreUnsaved = false;
687
        if (!$ignoreUnsaved) {
688
            $values = array_replace($values, $this->_unsaved);
689
        }
690
691
        // build the response
692
        $result = [];
693
        foreach ($properties as $k) {
694
            $accessor = self::getAccessor($k);
695
696
            // use the supplied value if it's available
697
            if (array_key_exists($k, $values)) {
698
                $result[$k] = $values[$k];
699
            // get relationship values
700
            } elseif (static::isRelationship($k)) {
701
                $result[$k] = $this->loadRelationship($k);
702
            // set any missing values to null
703
            } elseif ($this->hasProperty($k)) {
704
                $result[$k] = $this->_values[$k] = null;
705
            // throw an exception for non-properties that do not
706
            // have an accessor
707
            } 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...
708
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
709
            // otherwise the value is considered null
710
            } else {
711
                $result[$k] = null;
712
            }
713
714
            // call any accessors
715
            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...
716
                $result[$k] = $this->$accessor($result[$k]);
717
            }
718
        }
719
720
        return $result;
721
    }
722
723
    /**
724
     * Converts the model to an array.
725
     *
726
     * @return array model array
727
     */
728
    public function toArray()
729
    {
730
        // build the list of properties to retrieve
731
        $properties = $this->getProperties();
732
733
        // remove any hidden properties
734
        if (property_exists($this, 'hidden')) {
735
            $properties = array_diff($properties, static::$hidden);
736
        }
737
738
        // include any appended properties
739
        if (property_exists($this, 'appended')) {
740
            $properties = array_unique(array_merge($properties, static::$appended));
741
        }
742
743
        // get the values for the properties
744
        $result = $this->get($properties);
745
746
        // convert any models to arrays
747
        foreach ($result as &$value) {
748
            if ($value instanceof self) {
749
                $value = $value->toArray();
750
            }
751
        }
752
753
        return $result;
754
    }
755
756
    /////////////////////////////
757
    // Persistence
758
    /////////////////////////////
759
760
    /**
761
     * Saves the model.
762
     *
763
     * @return bool
764
     */
765
    public function save()
766
    {
767
        if (!$this->_persisted) {
768
            return $this->create();
769
        }
770
771
        return $this->set();
772
    }
773
774
    /**
775
     * Creates a new model.
776
     *
777
     * @param array $data optional key-value properties to set
778
     *
779
     * @return bool
780
     *
781
     * @throws BadMethodCallException when called on an existing model
782
     */
783
    public function create(array $data = [])
784
    {
785
        if ($this->_persisted) {
786
            throw new BadMethodCallException('Cannot call create() on an existing model');
787
        }
788
789
        // mass assign values passed into create()
790
        $this->setValues($data);
791
792
        // add in any preset values
793
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
794
795
        // dispatch the model.creating event
796
        $event = $this->dispatch(ModelEvent::CREATING);
797
        if ($event->isPropagationStopped()) {
798
            return false;
799
        }
800
801
        // validate the model
802
        if (!$this->valid()) {
803
            return false;
804
        }
805
806
        // create the model using the driver
807
        if (!self::getDriver()->createModel($this, $this->_unsaved)) {
808
            return false;
809
        }
810
811
        // update the model with the persisted values and new ID(s)
812
        $newValues = array_replace(
813
            $this->_unsaved,
814
            $this->getNewIds());
815
        $this->refreshWith($newValues);
816
817
        // dispatch the model.created event
818
        $event = $this->dispatch(ModelEvent::CREATED);
819
820
        return !$event->isPropagationStopped();
821
    }
822
823
    /**
824
     * Gets the IDs for a newly created model.
825
     *
826
     * @return string
827
     */
828
    protected function getNewIds()
829
    {
830
        $ids = [];
831
        foreach (static::$ids as $k) {
832
            // check if the ID property was already given,
833
            if (isset($this->_unsaved[$k])) {
834
                $ids[$k] = $this->_unsaved[$k];
835
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
836
            } else {
837
                $ids[$k] = self::getDriver()->getCreatedID($this, $k);
838
            }
839
        }
840
841
        return $ids;
842
    }
843
844
    /**
845
     * Updates the model.
846
     *
847
     * @param array $data optional key-value properties to set
848
     *
849
     * @return bool
850
     *
851
     * @throws BadMethodCallException when not called on an existing model
852
     */
853
    public function set(array $data = [])
854
    {
855
        if (!$this->_persisted) {
856
            throw new BadMethodCallException('Can only call set() on an existing model');
857
        }
858
859
        // mass assign values passed into set()
860
        $this->setValues($data);
861
862
        // not updating anything?
863
        if (count($this->_unsaved) === 0) {
864
            return true;
865
        }
866
867
        // dispatch the model.updating event
868
        $event = $this->dispatch(ModelEvent::UPDATING);
869
        if ($event->isPropagationStopped()) {
870
            return false;
871
        }
872
873
        // validate the model
874
        if (!$this->valid()) {
875
            return false;
876
        }
877
878
        // update the model using the driver
879
        if (!self::getDriver()->updateModel($this, $this->_unsaved)) {
880
            return false;
881
        }
882
883
        // update the model with the persisted values
884
        $this->refreshWith($this->_unsaved);
885
886
        // dispatch the model.updated event
887
        $event = $this->dispatch(ModelEvent::UPDATED);
888
889
        return !$event->isPropagationStopped();
890
    }
891
892
    /**
893
     * Delete the model.
894
     *
895
     * @return bool success
896
     */
897
    public function delete()
898
    {
899
        if (!$this->_persisted) {
900
            throw new BadMethodCallException('Can only call delete() on an existing model');
901
        }
902
903
        // dispatch the model.deleting event
904
        $event = $this->dispatch(ModelEvent::DELETING);
905
        if ($event->isPropagationStopped()) {
906
            return false;
907
        }
908
909
        $deleted = self::getDriver()->deleteModel($this);
910
911
        if ($deleted) {
912
            // dispatch the model.deleted event
913
            $event = $this->dispatch(ModelEvent::DELETED);
914
            if ($event->isPropagationStopped()) {
915
                return false;
916
            }
917
918
            $this->_persisted = false;
919
        }
920
921
        return $deleted;
922
    }
923
924
    /**
925
     * Tells if the model has been persisted.
926
     *
927
     * @return bool
928
     */
929
    public function persisted()
930
    {
931
        return $this->_persisted;
932
    }
933
934
    /**
935
     * Loads the model from the data layer.
936
     *
937
     * @return self
938
     *
939
     * @throws NotFoundException
940
     */
941
    public function refresh()
942
    {
943
        if (!$this->_persisted) {
944
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
945
        }
946
947
        $query = static::query();
948
        $query->where($this->ids());
949
950
        $values = self::getDriver()->queryModels($query);
951
952
        if (count($values) === 0) {
953
            return $this;
954
        }
955
956
        return $this->refreshWith($values[0]);
957
    }
958
959
    /**
960
     * Loads values into the model retrieved from the data layer.
961
     *
962
     * @param array $values values
963
     *
964
     * @return self
965
     */
966
    public function refreshWith(array $values)
967
    {
968
        // cast the values
969
        if (property_exists($this, 'casts')) {
970
            foreach ($values as $k => &$value) {
971
                if ($type = static::getPropertyType($k)) {
972
                    $value = static::cast($type, $value);
973
                }
974
            }
975
        }
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->_unsaved + $this->_values;
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