Completed
Push — master ( c9a8fa...8c952a )
by Jared
04:55
created

Model::create()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 39
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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