Completed
Push — master ( df56f5...48b3b9 )
by Jared
02:27
created

Model::getTablename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
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 $accessors = [];
120
121
    /**
122
     * @staticvar array
123
     */
124
    private static $mutators = [];
125
126
    /**
127
     * @var bool
128
     */
129
    private $_ignoreUnsaved;
130
131
    /**
132
     * Creates a new model object.
133
     *
134
     * @param array $values values to fill model with
135
     */
136
    public function __construct(array $values = [])
137
    {
138
        foreach ($values as $k => $v) {
139
            $this->setValue($k, $v, false);
140
        }
141
142
        // ensure the initialize function is called only once
143
        $k = get_called_class();
144
        if (!isset(self::$initialized[$k])) {
145
            $this->initialize();
146
            self::$initialized[$k] = true;
147
        }
148
    }
149
150
    /**
151
     * The initialize() method is called once per model. It's used
152
     * to perform any one-off tasks before the model gets
153
     * constructed. This is a great place to add any model
154
     * properties. When extending this method be sure to call
155
     * parent::initialize() as some important stuff happens here.
156
     * If extending this method to add properties then you should
157
     * call parent::initialize() after adding any properties.
158
     */
159
    protected function initialize()
160
    {
161
        // add in the default ID property
162
        if (static::$ids == [self::DEFAULT_ID_PROPERTY]) {
163
            if (property_exists($this, 'casts') && !isset(static::$casts[self::DEFAULT_ID_PROPERTY])) {
164
                static::$casts[self::DEFAULT_ID_PROPERTY] = self::TYPE_INTEGER;
165
            }
166
        }
167
168
        // generates created_at and updated_at timestamps
169
        if (property_exists($this, 'autoTimestamps')) {
170
            $this->installAutoTimestamps();
171
        }
172
    }
173
174
    private function installAutoTimestamps()
175
    {
176
        if (property_exists($this, 'casts')) {
177
            static::$casts['created_at'] = self::TYPE_DATE;
178
            static::$casts['updated_at'] = self::TYPE_DATE;
179
        }
180
181
        self::creating(function (ModelEvent $event) {
182
            $model = $event->getModel();
183
            $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...
184
            $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...
185
        });
186
187
        self::updating(function (ModelEvent $event) {
188
            $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...
189
        });
190
    }
191
192
    /**
193
     * Sets the adapter for all models.
194
     *
195
     * @param AdapterInterface $adapter
196
     */
197
    public static function setAdapter(AdapterInterface $adapter)
198
    {
199
        self::$adapter = $adapter;
200
    }
201
202
    /**
203
     * Gets the adapter for all models.
204
     *
205
     * @return AdapterInterface
206
     *
207
     * @throws AdapterMissingException
208
     */
209
    public static function getAdapter()
210
    {
211
        if (!self::$adapter) {
212
            throw new AdapterMissingException('A model adapter has not been set yet.');
213
        }
214
215
        return self::$adapter;
216
    }
217
218
    /**
219
     * Clears the adapter for all models.
220
     */
221
    public static function clearAdapter()
222
    {
223
        self::$adapter = null;
224
    }
225
226
    /**
227
     * Sets the locale instance for all models.
228
     *
229
     * @param Locale $locale
230
     */
231
    public static function setLocale(Locale $locale)
232
    {
233
        self::$locale = $locale;
234
    }
235
236
    /**
237
     * Gets the locale instance for all models.
238
     *
239
     * @return Locale
240
     */
241
    public static function getLocale()
242
    {
243
        return self::$locale;
244
    }
245
246
    /**
247
     * Clears the locale for all models.
248
     */
249
    public static function clearLocale()
250
    {
251
        self::$locale = null;
252
    }
253
254
    /**
255
     * Gets the name of the model without namespacing.
256
     *
257
     * @return string
258
     */
259
    public static function modelName()
260
    {
261
        return explode('\\', get_called_class())[0];
262
    }
263
264
    /**
265
     * Gets the table name of the model.
266
     *
267
     * @return string
268
     */
269
    public function getTablename()
270
    {
271
        $inflector = Inflector::get();
272
273
        return $inflector->camelize($inflector->pluralize(static::modelName()));
274
    }
275
276
    /**
277
     * Gets the model ID.
278
     *
279
     * @return string|number|null ID
280
     */
281
    public function id()
282
    {
283
        $ids = $this->ids();
284
285
        // if a single ID then return it
286
        if (count($ids) === 1) {
287
            return reset($ids);
288
        }
289
290
        // if multiple IDs then return a comma-separated list
291
        return implode(',', $ids);
292
    }
293
294
    /**
295
     * Gets a key-value map of the model ID.
296
     *
297
     * @return array ID map
298
     */
299
    public function ids()
300
    {
301
        return $this->get(static::$ids);
302
    }
303
304
    /////////////////////////////
305
    // Magic Methods
306
    /////////////////////////////
307
308
    public function __toString()
309
    {
310
        return get_called_class().'('.$this->id().')';
311
    }
312
313
    public function __get($name)
314
    {
315
        return array_values($this->get([$name]))[0];
316
    }
317
318
    public function __set($name, $value)
319
    {
320
        $this->setValue($name, $value);
321
    }
322
323
    public function __isset($name)
324
    {
325
        return array_key_exists($name, $this->_unsaved) || $this->hasProperty($name);
326
    }
327
328
    public function __unset($name)
329
    {
330
        if (static::isRelationship($name)) {
331
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
332
        }
333
334
        if (array_key_exists($name, $this->_unsaved)) {
335
            unset($this->_unsaved[$name]);
336
        }
337
    }
338
339
    public static function __callStatic($name, $parameters)
340
    {
341
        // Any calls to unkown static methods should be deferred to
342
        // the query. This allows calls like User::where()
343
        // 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...
344
        return call_user_func_array([static::query(), $name], $parameters);
345
    }
346
347
    /////////////////////////////
348
    // ArrayAccess Interface
349
    /////////////////////////////
350
351
    public function offsetExists($offset)
352
    {
353
        return isset($this->$offset);
354
    }
355
356
    public function offsetGet($offset)
357
    {
358
        return $this->$offset;
359
    }
360
361
    public function offsetSet($offset, $value)
362
    {
363
        $this->$offset = $value;
364
    }
365
366
    public function offsetUnset($offset)
367
    {
368
        unset($this->$offset);
369
    }
370
371
    /////////////////////////////
372
    // Property Definitions
373
    /////////////////////////////
374
375
    /**
376
     * Gets the names of the model ID properties.
377
     *
378
     * @return array
379
     */
380
    public static function getIdProperties()
381
    {
382
        return static::$ids;
383
    }
384
385
    /**
386
     * Builds an existing model instance given a single ID value or
387
     * ordered array of ID values.
388
     *
389
     * @param mixed $id
390
     *
391
     * @return Model
392
     */
393
    public static function buildFromId($id)
394
    {
395
        $ids = [];
396
        $id = (array) $id;
397
        foreach (static::$ids as $j => $k) {
398
            $ids[$k] = $id[$j];
399
        }
400
401
        $model = new static($ids);
402
403
        return $model;
404
    }
405
406
    /**
407
     * Gets the mutator method name for a given proeprty name.
408
     * Looks for methods in the form of `setPropertyValue`.
409
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
410
     *
411
     * @param string $property
412
     *
413
     * @return string|false method name if it exists
414
     */
415 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...
416
    {
417
        $class = get_called_class();
418
419
        $k = $class.':'.$property;
420
        if (!array_key_exists($k, self::$mutators)) {
421
            $inflector = Inflector::get();
422
            $method = 'set'.$inflector->camelize($property).'Value';
423
424
            if (!method_exists($class, $method)) {
425
                $method = false;
426
            }
427
428
            self::$mutators[$k] = $method;
429
        }
430
431
        return self::$mutators[$k];
432
    }
433
434
    /**
435
     * Gets the accessor method name for a given proeprty name.
436
     * Looks for methods in the form of `getPropertyValue`.
437
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
438
     *
439
     * @param string $property
440
     *
441
     * @return string|false method name if it exists
442
     */
443 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...
444
    {
445
        $class = get_called_class();
446
447
        $k = $class.':'.$property;
448
        if (!array_key_exists($k, self::$accessors)) {
449
            $inflector = Inflector::get();
450
            $method = 'get'.$inflector->camelize($property).'Value';
451
452
            if (!method_exists($class, $method)) {
453
                $method = false;
454
            }
455
456
            self::$accessors[$k] = $method;
457
        }
458
459
        return self::$accessors[$k];
460
    }
461
462
    /**
463
     * Checks if a given property is a relationship.
464
     *
465
     * @param string $property
466
     *
467
     * @return bool
468
     */
469
    public static function isRelationship($property)
470
    {
471
        return in_array($property, static::$relationships);
472
    }
473
474
    /**
475
     * Gets the string date format for a property. Defaults to
476
     * UNIX timestamps.
477
     *
478
     * @param string $property
479
     *
480
     * @return string
481
     */
482
    public static function getDateFormat($property)
483
    {
484
        if (isset(static::$dates[$property])) {
485
            return static::$dates[$property];
486
        }
487
488
        return self::DEFAULT_DATE_FORMAT;
489
    }
490
491
    /**
492
     * Gets the title of a property.
493
     *
494
     * @param string $name
495
     *
496
     * @return string
497
     */
498
    public static function getPropertyTitle($name)
499
    {
500
        // attmept to fetch the title from the Locale service
501
        $k = 'pulsar.properties.'.static::modelName().'.'.$name;
502
        if (self::$locale && $title = self::$locale->t($k)) {
503
            if ($title != $k) {
504
                return $title;
505
            }
506
        }
507
508
        return Inflector::get()->humanize($name);
509
    }
510
511
    /**
512
     * Gets the type cast for a property.
513
     *
514
     * @param string $property
515
     *
516
     * @return string|null
517
     */
518
    public static function getPropertyType($property)
519
    {
520
        if (property_exists(get_called_class(), 'casts')) {
521
            return array_value(static::$casts, $property);
522
        }
523
    }
524
525
    /**
526
     * Casts a value to a given type.
527
     *
528
     * @param string|null $type
529
     * @param mixed       $value
530
     * @param string      $property optional property name
531
     *
532
     * @return mixed casted value
533
     */
534
    public static function cast($type, $value, $property = null)
535
    {
536
        if ($value === null) {
537
            return;
538
        }
539
540
        switch ($type) {
541
        case self::TYPE_STRING:
542
            return (string) $value;
543
544
        case self::TYPE_INTEGER:
545
            return (int) $value;
546
547
        case self::TYPE_FLOAT:
548
            return (float) $value;
549
550
        case self::TYPE_BOOLEAN:
551
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
552
553
        case self::TYPE_DATE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
554
            // cast dates into Carbon objects
555
            if ($value instanceof Carbon) {
556
                return $value;
557
            } else {
558
                $format = self::getDateFormat($property);
559
560
                return Carbon::createFromFormat($format, $value);
561
            }
562
563 View Code Duplication
        case self::TYPE_ARRAY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
564
            // decode JSON into an array
565
            if (is_string($value)) {
566
                return json_decode($value, true);
567
            } else {
568
                return (array) $value;
569
            }
570
571 View Code Duplication
        case self::TYPE_OBJECT:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
572
            // decode JSON into an object
573
            if (is_string($value)) {
574
                return (object) json_decode($value);
575
            } else {
576
                return (object) $value;
577
            }
578
579
        default:
580
            return $value;
581
        }
582
    }
583
584
    /**
585
     * Gets the properties of this model.
586
     *
587
     * @return array
588
     */
589
    public function getProperties()
590
    {
591
        return array_unique(array_merge(
592
            static::$ids, array_keys($this->_values)));
593
    }
594
595
    /**
596
     * Checks if the model has a property.
597
     *
598
     * @param string $property
599
     *
600
     * @return bool has property
601
     */
602
    public function hasProperty($property)
603
    {
604
        return array_key_exists($property, $this->_values) ||
605
               in_array($property, static::$ids);
606
    }
607
608
    /////////////////////////////
609
    // Values
610
    /////////////////////////////
611
612
    /**
613
     * Sets an unsaved value.
614
     *
615
     * @param string $name
616
     * @param mixed  $value
617
     * @param bool   $unsaved when true, sets an unsaved value
618
     *
619
     * @throws BadMethodCallException when setting a relationship
620
     *
621
     * @return self
622
     */
623
    public function setValue($name, $value, $unsaved = true)
624
    {
625
        if (static::isRelationship($name)) {
626
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
627
        }
628
629
        // cast the value
630
        if ($type = static::getPropertyType($name)) {
631
            $value = static::cast($type, $value, $name);
632
        }
633
634
        // apply any mutators
635
        if ($mutator = self::getMutator($name)) {
636
            $value = $this->$mutator($value);
637
        }
638
639
        // save the value on the model property
640
        if ($unsaved) {
641
            $this->_unsaved[$name] = $value;
642
        } else {
643
            $this->_values[$name] = $value;
644
        }
645
646
        return $this;
647
    }
648
649
    /**
650
     * Sets a collection values on the model from an untrusted
651
     * input. Also known as mass assignment.
652
     *
653
     * @param array $values
654
     *
655
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
656
     *
657
     * @return self
658
     */
659
    public function setValues($values)
660
    {
661
        // check if the model has a mass assignment whitelist
662
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
663
664
        // if no whitelist, then check for a blacklist
665
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
666
667
        foreach ($values as $k => $value) {
668
            // check for mass assignment violations
669
            if (($permitted && !in_array($k, $permitted)) ||
670
                ($protected && in_array($k, $protected))) {
671
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
672
            }
673
674
            $this->setValue($k, $value);
675
        }
676
677
        return $this;
678
    }
679
680
    /**
681
     * Ignores unsaved values when fetching the next value.
682
     *
683
     * @return self
684
     */
685
    public function ignoreUnsaved()
686
    {
687
        $this->_ignoreUnsaved = true;
688
689
        return $this;
690
    }
691
692
    /**
693
     * Gets property values from the model.
694
     *
695
     * This method looks up values from these locations in this
696
     * precedence order (least important to most important):
697
     *  1. local values
698
     *  2. unsaved values
699
     *
700
     * @param array $properties list of property names to fetch values of
701
     *
702
     * @return array
703
     *
704
     * @throws InvalidArgumentException when a property was requested not present in the values
705
     */
706
    public function get(array $properties)
707
    {
708
        // load the values from the local model cache
709
        $values = $this->_values;
710
711
        // unless specified, use any unsaved values
712
        $ignoreUnsaved = $this->_ignoreUnsaved;
713
        $this->_ignoreUnsaved = false;
714
        if (!$ignoreUnsaved) {
715
            $values = array_replace($values, $this->_unsaved);
716
        }
717
718
        // build the response
719
        $result = [];
720
        foreach ($properties as $k) {
721
            $accessor = self::getAccessor($k);
722
723
            // use the supplied value if it's available
724
            if (array_key_exists($k, $values)) {
725
                $result[$k] = $values[$k];
726
            // get relationship values
727
            } elseif (static::isRelationship($k)) {
728
                $result[$k] = $this->loadRelationship($k);
729
            // set any missing values to null
730
            } elseif ($this->hasProperty($k)) {
731
                $result[$k] = $this->_values[$k] = null;
732
            // throw an exception for non-properties that do not
733
            // have an accessor
734
            } 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...
735
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
736
            // otherwise the value is considered null
737
            } else {
738
                $result[$k] = null;
739
            }
740
741
            // call any accessors
742
            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...
743
                $result[$k] = $this->$accessor($result[$k]);
744
            }
745
        }
746
747
        return $result;
748
    }
749
750
    /**
751
     * Converts the model to an array.
752
     *
753
     * @return array model array
754
     */
755
    public function toArray()
756
    {
757
        // build the list of properties to retrieve
758
        $properties = $this->getProperties();
759
760
        // remove any hidden properties
761
        if (property_exists($this, 'hidden')) {
762
            $properties = array_diff($properties, static::$hidden);
763
        }
764
765
        // include any appended properties
766
        if (property_exists($this, 'appended')) {
767
            $properties = array_unique(array_merge($properties, static::$appended));
768
        }
769
770
        // get the values for the properties
771
        $result = $this->get($properties);
772
773
        foreach ($result as $k => &$value) {
774
            // convert any models to arrays
775
            if ($value instanceof self) {
776
                $value = $value->toArray();
777
            // convert any Carbon objects to date strings
778
            } elseif ($value instanceof Carbon) {
779
                $format = self::getDateFormat($k);
780
                $value = $value->format($format);
781
            }
782
        }
783
784
        return $result;
785
    }
786
787
    /////////////////////////////
788
    // Persistence
789
    /////////////////////////////
790
791
    /**
792
     * Saves the model.
793
     *
794
     * @return bool
795
     */
796
    public function save()
797
    {
798
        if (!$this->_persisted) {
799
            return $this->create();
800
        }
801
802
        return $this->set();
803
    }
804
805
    /**
806
     * Creates a new model.
807
     *
808
     * @param array $data optional key-value properties to set
809
     *
810
     * @return bool
811
     *
812
     * @throws BadMethodCallException when called on an existing model
813
     */
814
    public function create(array $data = [])
815
    {
816
        if ($this->_persisted) {
817
            throw new BadMethodCallException('Cannot call create() on an existing model');
818
        }
819
820
        // mass assign values passed into create()
821
        $this->setValues($data);
822
823
        // add in any preset values
824
        $this->_unsaved = array_replace($this->_values, $this->_unsaved);
825
826
        // dispatch the model.creating event
827
        $event = $this->dispatch(ModelEvent::CREATING);
828
        if ($event->isPropagationStopped()) {
829
            return false;
830
        }
831
832
        // validate the model
833
        if (!$this->valid()) {
834
            return false;
835
        }
836
837
        // create the model using the adapter
838
        if (!self::getAdapter()->createModel($this, $this->_unsaved)) {
839
            return false;
840
        }
841
842
        // update the model with the persisted values and new ID(s)
843
        $newValues = array_replace(
844
            $this->_unsaved,
845
            $this->getNewIds());
846
        $this->refreshWith($newValues);
847
848
        // dispatch the model.created event
849
        $event = $this->dispatch(ModelEvent::CREATED);
850
851
        return !$event->isPropagationStopped();
852
    }
853
854
    /**
855
     * Gets the IDs for a newly created model.
856
     *
857
     * @return string
858
     */
859
    protected function getNewIds()
860
    {
861
        $ids = [];
862
        foreach (static::$ids as $k) {
863
            // check if the ID property was already given,
864
            if (isset($this->_unsaved[$k])) {
865
                $ids[$k] = $this->_unsaved[$k];
866
            // otherwise, get it from the data layer (i.e. auto-incrementing IDs)
867
            } else {
868
                $ids[$k] = self::getAdapter()->getCreatedID($this, $k);
869
            }
870
        }
871
872
        return $ids;
873
    }
874
875
    /**
876
     * Updates the model.
877
     *
878
     * @param array $data optional key-value properties to set
879
     *
880
     * @return bool
881
     *
882
     * @throws BadMethodCallException when not called on an existing model
883
     */
884
    public function set(array $data = [])
885
    {
886
        if (!$this->_persisted) {
887
            throw new BadMethodCallException('Can only call set() on an existing model');
888
        }
889
890
        // mass assign values passed into set()
891
        $this->setValues($data);
892
893
        // not updating anything?
894
        if (count($this->_unsaved) === 0) {
895
            return true;
896
        }
897
898
        // dispatch the model.updating event
899
        $event = $this->dispatch(ModelEvent::UPDATING);
900
        if ($event->isPropagationStopped()) {
901
            return false;
902
        }
903
904
        // validate the model
905
        if (!$this->valid()) {
906
            return false;
907
        }
908
909
        // update the model using the adapter
910
        if (!self::getAdapter()->updateModel($this, $this->_unsaved)) {
911
            return false;
912
        }
913
914
        // update the model with the persisted values
915
        $this->refreshWith($this->_unsaved);
916
917
        // dispatch the model.updated event
918
        $event = $this->dispatch(ModelEvent::UPDATED);
919
920
        return !$event->isPropagationStopped();
921
    }
922
923
    /**
924
     * Delete the model.
925
     *
926
     * @return bool success
927
     */
928
    public function delete()
929
    {
930
        if (!$this->_persisted) {
931
            throw new BadMethodCallException('Can only call delete() on an existing model');
932
        }
933
934
        // dispatch the model.deleting event
935
        $event = $this->dispatch(ModelEvent::DELETING);
936
        if ($event->isPropagationStopped()) {
937
            return false;
938
        }
939
940
        $deleted = self::getAdapter()->deleteModel($this);
941
942
        if ($deleted) {
943
            // dispatch the model.deleted event
944
            $event = $this->dispatch(ModelEvent::DELETED);
945
            if ($event->isPropagationStopped()) {
946
                return false;
947
            }
948
949
            $this->_persisted = false;
950
        }
951
952
        return $deleted;
953
    }
954
955
    /**
956
     * Tells if the model has been persisted.
957
     *
958
     * @return bool
959
     */
960
    public function persisted()
961
    {
962
        return $this->_persisted;
963
    }
964
965
    /**
966
     * Loads the model from the data layer.
967
     *
968
     * @return self
969
     *
970
     * @throws NotFoundException
971
     */
972
    public function refresh()
973
    {
974
        if (!$this->_persisted) {
975
            throw new NotFoundException('Cannot call refresh() before '.static::modelName().' has been persisted');
976
        }
977
978
        $query = static::query();
979
        $query->where($this->ids());
980
981
        $values = self::getAdapter()->queryModels($query);
982
983
        if (count($values) === 0) {
984
            return $this;
985
        }
986
987
        return $this->refreshWith($values[0]);
988
    }
989
990
    /**
991
     * Loads values into the model retrieved from the data layer.
992
     *
993
     * @param array $values values
994
     *
995
     * @return self
996
     */
997
    public function refreshWith(array $values)
998
    {
999
        // cast the values
1000
        if (property_exists($this, 'casts')) {
1001
            foreach ($values as $k => &$value) {
1002
                if ($type = static::getPropertyType($k)) {
1003
                    $value = static::cast($type, $value, $k);
1004
                }
1005
            }
1006
        }
1007
1008
        $this->_persisted = true;
1009
        $this->_values = $values;
1010
        $this->_unsaved = [];
1011
1012
        return $this;
1013
    }
1014
1015
    /////////////////////////////
1016
    // Queries
1017
    /////////////////////////////
1018
1019
    /**
1020
     * Generates a new query instance.
1021
     *
1022
     * @return Query
1023
     */
1024
    public static function query()
1025
    {
1026
        // Create a new model instance for the query to ensure
1027
        // that the model's initialize() method gets called.
1028
        // Otherwise, the property definitions will be incomplete.
1029
        $model = new static();
1030
1031
        return new Query($model);
1032
    }
1033
1034
    /**
1035
     * Finds a single instance of a model given it's ID.
1036
     *
1037
     * @param mixed $id
1038
     *
1039
     * @return Model|null
1040
     */
1041
    public static function find($id)
1042
    {
1043
        $model = static::buildFromId($id);
1044
1045
        return static::query()->where($model->ids())->first();
1046
    }
1047
1048
    /**
1049
     * Finds a single instance of a model given it's ID or throws an exception.
1050
     *
1051
     * @param mixed $id
1052
     *
1053
     * @return Model|false
1054
     *
1055
     * @throws NotFoundException when a model could not be found
1056
     */
1057
    public static function findOrFail($id)
1058
    {
1059
        $model = static::find($id);
1060
        if (!$model) {
1061
            throw new NotFoundException('Could not find the requested '.static::modelName());
1062
        }
1063
1064
        return $model;
1065
    }
1066
1067
    /**
1068
     * Gets the toal number of records matching an optional criteria.
1069
     *
1070
     * @param array $where criteria
1071
     *
1072
     * @return int total
1073
     */
1074
    public static function totalRecords(array $where = [])
1075
    {
1076
        $query = static::query();
1077
        $query->where($where);
1078
1079
        return self::getAdapter()->totalRecords($query);
1080
    }
1081
1082
    /////////////////////////////
1083
    // Relationships
1084
    /////////////////////////////
1085
1086
    /**
1087
     * Creates the parent side of a One-To-One relationship.
1088
     *
1089
     * @param string $model      foreign model class
1090
     * @param string $foreignKey identifying key on foreign model
1091
     * @param string $localKey   identifying key on local model
1092
     *
1093
     * @return \Pulsar\Relation\Relation
1094
     */
1095
    public function hasOne($model, $foreignKey = '', $localKey = '')
1096
    {
1097
        // the default foreign key would look like `user_id`
1098
        // for a model named User
1099
        if (!$foreignKey) {
1100
            $inflector = Inflector::get();
1101
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1102
        }
1103
1104
        if (!$localKey) {
1105
            $localKey = self::DEFAULT_ID_PROPERTY;
1106
        }
1107
1108
        return new HasOne($this, $localKey, $model, $foreignKey, $localKey);
0 ignored issues
show
Unused Code introduced by
The call to HasOne::__construct() has too many arguments starting with $localKey.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1109
    }
1110
1111
    /**
1112
     * Creates the child side of a One-To-One or One-To-Many relationship.
1113
     *
1114
     * @param string $model      foreign model class
1115
     * @param string $foreignKey identifying key on foreign model
1116
     * @param string $localKey   identifying key on local model
1117
     *
1118
     * @return \Pulsar\Relation\Relation
1119
     */
1120 View Code Duplication
    public function belongsTo($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1121
    {
1122
        if (!$foreignKey) {
1123
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1124
        }
1125
1126
        // the default local key would look like `user_id`
1127
        // for a model named User
1128
        if (!$localKey) {
1129
            $inflector = Inflector::get();
1130
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1131
        }
1132
1133
        return new BelongsTo($this, $localKey, $model, $foreignKey);
1134
    }
1135
1136
    /**
1137
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1138
     *
1139
     * @param string $model      foreign model class
1140
     * @param string $foreignKey identifying key on foreign model
1141
     * @param string $localKey   identifying key on local model
1142
     *
1143
     * @return \Pulsar\Relation\Relation
1144
     */
1145 View Code Duplication
    public function hasMany($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1146
    {
1147
        // the default foreign key would look like `user_id`
1148
        // for a model named User
1149
        if (!$foreignKey) {
1150
            $inflector = Inflector::get();
1151
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1152
        }
1153
1154
        if (!$localKey) {
1155
            $localKey = self::DEFAULT_ID_PROPERTY;
1156
        }
1157
1158
        return new HasMany($this, $localKey, $model, $foreignKey);
1159
    }
1160
1161
    /**
1162
     * Creates the child side of a Many-To-Many relationship.
1163
     *
1164
     * @param string $model      foreign model class
1165
     * @param string $tablename  pivot table name
1166
     * @param string $foreignKey identifying key on foreign model
1167
     * @param string $localKey   identifying key on local model
1168
     *
1169
     * @return \Pulsar\Relation\Relation
1170
     */
1171
    public function belongsToMany($model, $tablename = '', $foreignKey = '', $localKey = '')
1172
    {
1173
        // the default pivot table name looks like
1174
        // RoleUser for models named Role and User.
1175
        // the tablename is built from the model names
1176
        // in alphabetic order.
1177
        if (!$tablename) {
1178
            $names = [$this::modelName(), $model::modelName()];
1179
            sort($names);
1180
            $tablename = implode($names);
1181
        }
1182
1183
        if (!$foreignKey) {
1184
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1185
        }
1186
1187
        // the default local key would look like `user_id`
1188
        // for a model named User
1189
        if (!$localKey) {
1190
            $inflector = Inflector::get();
1191
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1192
        }
1193
1194
        return new BelongsToMany($this, $localKey, $tablename, $model, $foreignKey);
1195
    }
1196
1197
    /**
1198
     * Loads a given relationship (if not already) and returns
1199
     * its results.
1200
     *
1201
     * @param string $name
1202
     *
1203
     * @return mixed
1204
     */
1205
    protected function loadRelationship($name)
1206
    {
1207
        if (!isset($this->_values[$name])) {
1208
            $relationship = $this->$name();
1209
            $this->_values[$name] = $relationship->getResults();
1210
        }
1211
1212
        return $this->_values[$name];
1213
    }
1214
1215
    /////////////////////////////
1216
    // Events
1217
    /////////////////////////////
1218
1219
    /**
1220
     * Gets the event dispatcher.
1221
     *
1222
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1223
     */
1224
    public static function getDispatcher($ignoreCache = false)
1225
    {
1226
        $class = get_called_class();
1227
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1228
            self::$dispatchers[$class] = new EventDispatcher();
1229
        }
1230
1231
        return self::$dispatchers[$class];
1232
    }
1233
1234
    /**
1235
     * Subscribes to a listener to an event.
1236
     *
1237
     * @param string   $event    event name
1238
     * @param callable $listener
1239
     * @param int      $priority optional priority, higher #s get called first
1240
     */
1241
    public static function listen($event, callable $listener, $priority = 0)
1242
    {
1243
        static::getDispatcher()->addListener($event, $listener, $priority);
1244
    }
1245
1246
    /**
1247
     * Adds a listener to the model.creating event.
1248
     *
1249
     * @param callable $listener
1250
     * @param int      $priority
1251
     */
1252
    public static function creating(callable $listener, $priority = 0)
1253
    {
1254
        static::listen(ModelEvent::CREATING, $listener, $priority);
1255
    }
1256
1257
    /**
1258
     * Adds a listener to the model.created event.
1259
     *
1260
     * @param callable $listener
1261
     * @param int      $priority
1262
     */
1263
    public static function created(callable $listener, $priority = 0)
1264
    {
1265
        static::listen(ModelEvent::CREATED, $listener, $priority);
1266
    }
1267
1268
    /**
1269
     * Adds a listener to the model.updating event.
1270
     *
1271
     * @param callable $listener
1272
     * @param int      $priority
1273
     */
1274
    public static function updating(callable $listener, $priority = 0)
1275
    {
1276
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1277
    }
1278
1279
    /**
1280
     * Adds a listener to the model.updated event.
1281
     *
1282
     * @param callable $listener
1283
     * @param int      $priority
1284
     */
1285
    public static function updated(callable $listener, $priority = 0)
1286
    {
1287
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1288
    }
1289
1290
    /**
1291
     * Adds a listener to the model.deleting event.
1292
     *
1293
     * @param callable $listener
1294
     * @param int      $priority
1295
     */
1296
    public static function deleting(callable $listener, $priority = 0)
1297
    {
1298
        static::listen(ModelEvent::DELETING, $listener, $priority);
1299
    }
1300
1301
    /**
1302
     * Adds a listener to the model.deleted event.
1303
     *
1304
     * @param callable $listener
1305
     * @param int      $priority
1306
     */
1307
    public static function deleted(callable $listener, $priority = 0)
1308
    {
1309
        static::listen(ModelEvent::DELETED, $listener, $priority);
1310
    }
1311
1312
    /**
1313
     * Dispatches an event.
1314
     *
1315
     * @param string $eventName
1316
     *
1317
     * @return ModelEvent
1318
     */
1319
    protected function dispatch($eventName)
1320
    {
1321
        $event = new ModelEvent($this);
1322
1323
        return static::getDispatcher()->dispatch($eventName, $event);
1324
    }
1325
1326
    /////////////////////////////
1327
    // Validation
1328
    /////////////////////////////
1329
1330
    /**
1331
     * Gets the error stack for this model instance. Used to
1332
     * keep track of validation errors.
1333
     *
1334
     * @return Errors
1335
     */
1336
    public function errors()
1337
    {
1338
        if (!$this->_errors) {
1339
            $this->_errors = new Errors($this, self::$locale);
1340
        }
1341
1342
        return $this->_errors;
1343
    }
1344
1345
    /**
1346
     * Checks if the model is valid in its current state.
1347
     *
1348
     * @return bool
1349
     */
1350
    public function valid()
1351
    {
1352
        // clear any previous errors
1353
        $this->errors()->clear();
1354
1355
        // run the validator against the model values
1356
        $validator = $this->getValidator();
1357
        $values = $this->_unsaved + $this->_values;
1358
        $validated = $validator->validate($values);
1359
1360
        // add back any modified unsaved values
1361
        foreach (array_keys($this->_unsaved) as $k) {
1362
            $this->_unsaved[$k] = $values[$k];
1363
        }
1364
1365
        return $validated;
1366
    }
1367
1368
    /**
1369
     * Gets a new validator instance for this model.
1370
     * 
1371
     * @return Validator
1372
     */
1373
    public function getValidator()
1374
    {
1375
        return new Validator(static::$validations, $this->errors());
1376
    }
1377
}
1378