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