Completed
Push — master ( f7d9c3...6982c8 )
by Jared
02:02
created

Model::creating()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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