Completed
Push — master ( a02b53...54bf83 )
by Jared
02:57
created

Model::refreshWith()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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