Completed
Push — master ( 9410dd...370a5b )
by Jared
02:53
created

Model::getValues()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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