Completed
Push — master ( c11597...391ce1 )
by Jared
02:08
created

Model::ids()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 0
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use BadMethodCallException;
15
use ICanBoogie\Inflector;
16
use Pimple\Container;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Exception\DriverMissingException;
19
use Pulsar\Exception\ModelNotFoundException;
20
use Pulsar\Relation\BelongsTo;
21
use Pulsar\Relation\BelongsToMany;
22
use Pulsar\Relation\HasMany;
23
use Pulsar\Relation\HasOne;
24
use Symfony\Component\EventDispatcher\EventDispatcher;
25
26
/**
27
 * Class Model.
28
 */
29
abstract class Model implements \ArrayAccess
30
{
31
    const IMMUTABLE = 0;
32
    const MUTABLE_CREATE_ONLY = 1;
33
    const MUTABLE = 2;
34
35
    const TYPE_STRING = 'string';
36
    const TYPE_NUMBER = 'number'; // DEPRECATED
37
    const TYPE_INTEGER = 'integer';
38
    const TYPE_FLOAT = 'float';
39
    const TYPE_BOOLEAN = 'boolean';
40
    const TYPE_DATE = 'date';
41
    const TYPE_OBJECT = 'object';
42
    const TYPE_ARRAY = 'array';
43
44
    const ERROR_REQUIRED_FIELD_MISSING = 'required_field_missing';
45
    const ERROR_VALIDATION_FAILED = 'validation_failed';
46
    const ERROR_NOT_UNIQUE = 'not_unique';
47
48
    const DEFAULT_ID_PROPERTY = 'id';
49
50
    /////////////////////////////
51
    // Model visible variables
52
    /////////////////////////////
53
54
    /**
55
     * List of model ID property names.
56
     *
57
     * @var array
58
     */
59
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
60
61
    /**
62
     * Property definitions expressed as a key-value map with
63
     * property names as the keys.
64
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
65
     *
66
     * @var array
67
     */
68
    protected static $properties = [];
69
70
    /**
71
     * @var Container
72
     */
73
    protected static $injectedApp;
74
75
    /**
76
     * @var array
77
     */
78
    protected static $dispatchers;
79
80
    /**
81
     * @var number|string|bool
82
     */
83
    protected $_id;
84
85
    /**
86
     * @var Container
87
     */
88
    protected $app;
89
90
    /**
91
     * @var array
92
     */
93
    protected $_values = [];
94
95
    /**
96
     * @var array
97
     */
98
    protected $_unsaved = [];
99
100
    /**
101
     * @var bool
102
     */
103
    protected $_persisted = false;
104
105
    /**
106
     * @var array
107
     */
108
    protected $_relationships = [];
109
110
    /////////////////////////////
111
    // Base model variables
112
    /////////////////////////////
113
114
    /**
115
     * @var array
116
     */
117
    private static $propertyDefinitionBase = [
118
        'type' => self::TYPE_STRING,
119
        'mutable' => self::MUTABLE,
120
        'null' => false,
121
        'unique' => false,
122
        'required' => false,
123
    ];
124
125
    /**
126
     * @var array
127
     */
128
    private static $defaultIDProperty = [
129
        'type' => self::TYPE_INTEGER,
130
        'mutable' => self::IMMUTABLE,
131
    ];
132
133
    /**
134
     * @var array
135
     */
136
    private static $timestampProperties = [
137
        'created_at' => [
138
            'type' => self::TYPE_DATE,
139
            'validate' => 'timestamp|db_timestamp',
140
        ],
141
        'updated_at' => [
142
            'type' => self::TYPE_DATE,
143
            'validate' => 'timestamp|db_timestamp',
144
        ],
145
    ];
146
147
    /**
148
     * @var array
149
     */
150
    private static $initialized = [];
151
152
    /**
153
     * @var DriverInterface
154
     */
155
    private static $driver;
156
157
    /**
158
     * @var array
159
     */
160
    private static $accessors = [];
161
162
    /**
163
     * @var array
164
     */
165
    private static $mutators = [];
166
167
    /**
168
     * @var bool
169
     */
170
    private $_ignoreUnsaved;
171
172
    /**
173
     * Creates a new model object.
174
     *
175
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
176
     * @param array                    $values optional key-value map to pre-seed model
177
     */
178
    public function __construct($id = false, array $values = [])
179
    {
180
        // initialize the model
181
        $this->app = self::$injectedApp;
182
        $this->init();
183
184
        // TODO need to store the id as an array
185
        // instead of a string to maintain type integrity
186
        if (is_array($id)) {
187
            // A model can be supplied as a primary key
188
            foreach ($id as &$el) {
189
                if ($el instanceof self) {
190
                    $el = $el->id();
191
                }
192
            }
193
194
            $id = implode(',', $id);
195
        // A model can be supplied as a primary key
196
        } elseif ($id instanceof self) {
197
            $id = $id->id();
198
        }
199
200
        $this->_id = $id;
201
202
        // load any given values
203
        if (count($values) > 0) {
204
            $this->refreshWith($values);
205
        }
206
    }
207
208
    /**
209
     * Performs initialization on this model.
210
     */
211
    private function init()
212
    {
213
        // ensure the initialize function is called only once
214
        $k = get_called_class();
215
        if (!isset(self::$initialized[$k])) {
216
            $this->initialize();
217
            self::$initialized[$k] = true;
218
        }
219
    }
220
221
    /**
222
     * The initialize() method is called once per model. It's used
223
     * to perform any one-off tasks before the model gets
224
     * constructed. This is a great place to add any model
225
     * properties. When extending this method be sure to call
226
     * parent::initialize() as some important stuff happens here.
227
     * If extending this method to add properties then you should
228
     * call parent::initialize() after adding any properties.
229
     */
230
    protected function initialize()
231
    {
232
        // load the driver
233
        static::getDriver();
234
235
        // add in the default ID property
236
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
237
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
238
        }
239
240
        // generates created_at and updated_at timestamps
241
        if (property_exists($this, 'autoTimestamps')) {
242
            $this->installAutoTimestamps();
243
        }
244
245
        // fill in each property by extending the property
246
        // definition base
247
        foreach (static::$properties as &$property) {
248
            $property = array_replace(self::$propertyDefinitionBase, $property);
249
        }
250
251
        // order the properties array by name for consistency
252
        // since it is constructed in a random order
253
        ksort(static::$properties);
254
    }
255
256
    /**
257
     * Installs the `created_at` and `updated_at` properties.
258
     */
259
    private function installAutoTimestamps()
260
    {
261
        static::$properties = array_replace(self::$timestampProperties, static::$properties);
262
263
        self::creating(function (ModelEvent $event) {
264
            $model = $event->getModel();
265
            $model->created_at = time();
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...
266
            $model->updated_at = time();
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...
267
        });
268
269
        self::updating(function (ModelEvent $event) {
270
            $event->getModel()->updated_at = time();
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...
271
        });
272
    }
273
274
    /**
275
     * Injects a DI container.
276
     *
277
     * @param Container $app
278
     */
279
    public static function inject(Container $app)
280
    {
281
        self::$injectedApp = $app;
282
    }
283
284
    /**
285
     * Gets the DI container used for this model.
286
     *
287
     * @return Container
288
     */
289
    public function getApp()
290
    {
291
        return $this->app;
292
    }
293
294
    /**
295
     * Sets the driver for all models.
296
     *
297
     * @param DriverInterface $driver
298
     */
299
    public static function setDriver(DriverInterface $driver)
300
    {
301
        self::$driver = $driver;
302
    }
303
304
    /**
305
     * Gets the driver for all models.
306
     *
307
     * @return DriverInterface
308
     *
309
     * @throws DriverMissingException when a driver has not been set yet
310
     */
311
    public static function getDriver()
312
    {
313
        if (!self::$driver) {
314
            throw new DriverMissingException('A model driver has not been set yet.');
315
        }
316
317
        return self::$driver;
318
    }
319
320
    /**
321
     * Clears the driver for all models.
322
     */
323
    public static function clearDriver()
324
    {
325
        self::$driver = null;
326
    }
327
328
    /**
329
     * Gets the name of the model, i.e. User.
330
     *
331
     * @return string
332
     */
333
    public static function modelName()
334
    {
335
        // strip namespacing
336
        $paths = explode('\\', get_called_class());
337
338
        return end($paths);
339
    }
340
341
    /**
342
     * Gets the model ID.
343
     *
344
     * @return string|number|false ID
345
     */
346
    public function id()
347
    {
348
        return $this->_id;
349
    }
350
351
    /**
352
     * Gets a key-value map of the model ID.
353
     *
354
     * @return array ID map
355
     */
356
    public function ids()
357
    {
358
        $return = [];
359
360
        // match up id values from comma-separated id string with property names
361
        $ids = explode(',', $this->_id);
362
        $ids = array_reverse($ids);
363
364
        // TODO need to store the id as an array
365
        // instead of a string to maintain type integrity
366
        foreach (static::$ids as $k => $f) {
367
            $id = (count($ids) > 0) ? array_pop($ids) : false;
368
369
            $return[$f] = $id;
370
        }
371
372
        return $return;
373
    }
374
375
    /////////////////////////////
376
    // Magic Methods
377
    /////////////////////////////
378
379
    /**
380
     * Converts the model into a string.
381
     *
382
     * @return string
383
     */
384
    public function __toString()
385
    {
386
        return get_called_class().'('.$this->_id.')';
387
    }
388
389
    /**
390
     * Shortcut to a get() call for a given property.
391
     *
392
     * @param string $name
393
     *
394
     * @return mixed
395
     */
396
    public function __get($name)
397
    {
398
        $result = $this->get([$name]);
399
400
        return reset($result);
401
    }
402
403
    /**
404
     * Sets an unsaved value.
405
     *
406
     * @param string $name
407
     * @param mixed  $value
408
     */
409
    public function __set($name, $value)
410
    {
411
        // if changing property, remove relation model
412
        if (isset($this->_relationships[$name])) {
413
            unset($this->_relationships[$name]);
414
        }
415
416
        // call any mutators
417
        $mutator = self::getMutator($name);
418
        if ($mutator) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mutator 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...
419
            $this->_unsaved[$name] = $this->$mutator($value);
420
        } else {
421
            $this->_unsaved[$name] = $value;
422
        }
423
    }
424
425
    /**
426
     * Checks if an unsaved value or property exists by this name.
427
     *
428
     * @param string $name
429
     *
430
     * @return bool
431
     */
432
    public function __isset($name)
433
    {
434
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
435
    }
436
437
    /**
438
     * Unsets an unsaved value.
439
     *
440
     * @param string $name
441
     */
442
    public function __unset($name)
443
    {
444
        if (array_key_exists($name, $this->_unsaved)) {
445
            // if changing property, remove relation model
446
            if (isset($this->_relationships[$name])) {
447
                unset($this->_relationships[$name]);
448
            }
449
450
            unset($this->_unsaved[$name]);
451
        }
452
    }
453
454
    /////////////////////////////
455
    // ArrayAccess Interface
456
    /////////////////////////////
457
458
    public function offsetExists($offset)
459
    {
460
        return isset($this->$offset);
461
    }
462
463
    public function offsetGet($offset)
464
    {
465
        return $this->$offset;
466
    }
467
468
    public function offsetSet($offset, $value)
469
    {
470
        $this->$offset = $value;
471
    }
472
473
    public function offsetUnset($offset)
474
    {
475
        unset($this->$offset);
476
    }
477
478
    public static function __callStatic($name, $parameters)
479
    {
480
        // Any calls to unkown static methods should be deferred to
481
        // the query. This allows calls like User::where()
482
        // 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...
483
        return call_user_func_array([static::query(), $name], $parameters);
484
    }
485
486
    /////////////////////////////
487
    // Property Definitions
488
    /////////////////////////////
489
490
    /**
491
     * Gets all the property definitions for the model.
492
     *
493
     * @return array key-value map of properties
494
     */
495
    public static function getProperties()
496
    {
497
        return static::$properties;
498
    }
499
500
    /**
501
     * Gets a property defition for the model.
502
     *
503
     * @param string $property property to lookup
504
     *
505
     * @return array|null property
506
     */
507
    public static function getProperty($property)
508
    {
509
        return array_value(static::$properties, $property);
510
    }
511
512
    /**
513
     * Gets the names of the model ID properties.
514
     *
515
     * @return array
516
     */
517
    public static function getIDProperties()
518
    {
519
        return static::$ids;
520
    }
521
522
    /**
523
     * Checks if the model has a property.
524
     *
525
     * @param string $property property
526
     *
527
     * @return bool has property
528
     */
529
    public static function hasProperty($property)
530
    {
531
        return isset(static::$properties[$property]);
532
    }
533
534
    /**
535
     * Gets the mutator method name for a given proeprty name.
536
     * Looks for methods in the form of `setPropertyValue`.
537
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
538
     *
539
     * @param string $property property
540
     *
541
     * @return string|false method name if it exists
542
     */
543 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...
544
    {
545
        $class = get_called_class();
546
547
        $k = $class.':'.$property;
548
        if (!array_key_exists($k, self::$mutators)) {
549
            $inflector = Inflector::get();
550
            $method = 'set'.$inflector->camelize($property).'Value';
551
552
            if (!method_exists($class, $method)) {
553
                $method = false;
554
            }
555
556
            self::$mutators[$k] = $method;
557
        }
558
559
        return self::$mutators[$k];
560
    }
561
562
    /**
563
     * Gets the accessor method name for a given proeprty name.
564
     * Looks for methods in the form of `getPropertyValue`.
565
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
566
     *
567
     * @param string $property property
568
     *
569
     * @return string|false method name if it exists
570
     */
571 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...
572
    {
573
        $class = get_called_class();
574
575
        $k = $class.':'.$property;
576
        if (!array_key_exists($k, self::$accessors)) {
577
            $inflector = Inflector::get();
578
            $method = 'get'.$inflector->camelize($property).'Value';
579
580
            if (!method_exists($class, $method)) {
581
                $method = false;
582
            }
583
584
            self::$accessors[$k] = $method;
585
        }
586
587
        return self::$accessors[$k];
588
    }
589
590
    /////////////////////////////
591
    // CRUD Operations
592
    /////////////////////////////
593
594
    /**
595
     * Saves the model.
596
     *
597
     * @return bool true when the operation was successful
598
     */
599
    public function save()
600
    {
601
        if ($this->_id === false) {
602
            return $this->create();
603
        }
604
605
        return $this->set();
606
    }
607
608
    /**
609
     * Creates a new model.
610
     *
611
     * @param array $data optional key-value properties to set
612
     *
613
     * @return bool true when the operation was successful
614
     *
615
     * @throws BadMethodCallException when called on an existing model
616
     */
617
    public function create(array $data = [])
618
    {
619
        if ($this->_id !== false) {
620
            throw new BadMethodCallException('Cannot call create() on an existing model');
621
        }
622
623
        // mass assign values passed into create()
624
        $this->setValues($data);
625
626
        // dispatch the model.creating event
627
        if (!$this->handleDispatch(ModelEvent::CREATING)) {
628
            return false;
629
        }
630
631
        $requiredProperties = [];
632
        foreach (static::$properties as $name => $property) {
633
            // build a list of the required properties
634
            if ($property['required']) {
635
                $requiredProperties[] = $name;
636
            }
637
638
            // add in default values
639
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
640
                $this->_unsaved[$name] = $property['default'];
641
            }
642
        }
643
644
        // validate the values being saved
645
        $validated = true;
646
        $insertArray = [];
647
        foreach ($this->_unsaved as $name => $value) {
648
            // exclude if value does not map to a property
649
            if (!isset(static::$properties[$name])) {
650
                continue;
651
            }
652
653
            $property = static::$properties[$name];
654
655
            // cannot insert immutable values
656
            // (unless using the default value)
657
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
658
                continue;
659
            }
660
661
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
662
            $insertArray[$name] = $value;
663
        }
664
665
        // check for required fields
666
        foreach ($requiredProperties as $name) {
667
            if (!isset($insertArray[$name])) {
668
                $property = static::$properties[$name];
669
                $this->app['errors']->push([
670
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
671
                    'params' => [
672
                        'field' => $name,
673
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
674
675
                $validated = false;
676
            }
677
        }
678
679
        if (!$validated) {
680
            return false;
681
        }
682
683
        $created = self::$driver->createModel($this, $insertArray);
684
685
        if ($created) {
686
            // determine the model's new ID
687
            $this->_id = $this->getNewID();
688
689
            // NOTE clear the local cache before the model.created
690
            // event so that fetching values forces a reload
691
            // from the storage layer
692
            $this->clearCache();
693
694
            // dispatch the model.created event
695
            if (!$this->handleDispatch(ModelEvent::CREATED)) {
696
                return false;
697
            }
698
        }
699
700
        return $created;
701
    }
702
703
    /**
704
     * Ignores unsaved values when fetching the next value.
705
     *
706
     * @return self
707
     */
708
    public function ignoreUnsaved()
709
    {
710
        $this->_ignoreUnsaved = true;
711
712
        return $this;
713
    }
714
715
    /**
716
     * Fetches property values from the model.
717
     *
718
     * This method looks up values in this order:
719
     * IDs, local cache, unsaved values, storage layer, defaults
720
     *
721
     * @param array $properties list of property names to fetch values of
722
     *
723
     * @return array
724
     */
725
    public function get(array $properties)
726
    {
727
        // load the values from the IDs and local model cache
728
        $values = array_replace($this->ids(), $this->_values);
729
730
        // unless specified, use any unsaved values
731
        $ignoreUnsaved = $this->_ignoreUnsaved;
732
        $this->_ignoreUnsaved = false;
733
734
        if (!$ignoreUnsaved) {
735
            $values = array_replace($values, $this->_unsaved);
736
        }
737
738
        // attempt to load any missing values from the storage layer
739
        $numMissing = count(array_diff($properties, array_keys($values)));
740
        if ($numMissing > 0) {
741
            $this->refresh();
742
            $values = array_replace($values, $this->_values);
743
744
            if (!$ignoreUnsaved) {
745
                $values = array_replace($values, $this->_unsaved);
746
            }
747
        }
748
749
        // build a key-value map of the requested properties
750
        $return = [];
751
        foreach ($properties as $k) {
752
            if (array_key_exists($k, $values)) {
753
                $return[$k] = $values[$k];
754
            // set any missing values to the default value
755
            } elseif (static::hasProperty($k)) {
756
                $return[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
757
            // use null for values of non-properties
758
            } else {
759
                $return[$k] = null;
760
            }
761
762
            // call any accessors
763
            if ($accessor = self::getAccessor($k)) {
764
                $return[$k] = $this->$accessor($return[$k]);
765
            }
766
        }
767
768
        return $return;
769
    }
770
771
    /**
772
     * Gets the ID for a newly created model.
773
     *
774
     * @return string
775
     */
776
    protected function getNewID()
777
    {
778
        $ids = [];
779
        foreach (static::$ids as $k) {
780
            // attempt use the supplied value if the ID property is mutable
781
            $property = static::getProperty($k);
782
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
783
                $ids[] = $this->_unsaved[$k];
784
            } else {
785
                $ids[] = self::$driver->getCreatedID($this, $k);
786
            }
787
        }
788
789
        // TODO need to store the id as an array
790
        // instead of a string to maintain type integrity
791
        return (count($ids) > 1) ? implode(',', $ids) : $ids[0];
792
    }
793
794
    /**
795
     * Sets a collection values on the model from an untrusted input.
796
     *
797
     * @param array $values
798
     *
799
     * @return self
800
     */
801
    public function setValues($values)
802
    {
803
        foreach ($values as $k => $value) {
804
            // TODO check for mass assignment violations
805
            $this->$k = $value;
806
        }
807
808
        return $this;
809
    }
810
811
    /**
812
     * Converts the model to an array.
813
     *
814
     * @return array
815
     */
816
    public function toArray()
817
    {
818
        // build the list of properties to retrieve
819
        $properties = array_keys(static::$properties);
820
821
        // remove any hidden properties
822
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
823
        $properties = array_diff($properties, $hide);
824
825
        // add any appended properties
826
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
827
        $properties = array_merge($properties, $append);
828
829
        // get the values for the properties
830
        $result = $this->get($properties);
831
832
        // apply the transformation hook
833
        if (method_exists($this, 'toArrayHook')) {
834
            $this->toArrayHook($result, [], [], []);
0 ignored issues
show
Bug introduced by
The method toArrayHook() does not exist on Pulsar\Model. Did you maybe mean toArray()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
835
        }
836
837
        return $result;
838
    }
839
840
    /**
841
     * Updates the model.
842
     *
843
     * @param array $data optional key-value properties to set
844
     *
845
     * @return bool true when the operation was successful
846
     *
847
     * @throws BadMethodCallException when not called on an existing model
848
     */
849
    public function set(array $data = [])
850
    {
851
        if ($this->_id === false) {
852
            throw new BadMethodCallException('Can only call set() on an existing model');
853
        }
854
855
        // mass assign values passed into set()
856
        $this->setValues($data);
857
858
        // not updating anything?
859
        if (count($this->_unsaved) == 0) {
860
            return true;
861
        }
862
863
        // dispatch the model.updating event
864
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
865
            return false;
866
        }
867
868
        // DEPRECATED
869
        if (method_exists($this, 'preSetHook') && !$this->preSetHook($this->_unsaved)) {
0 ignored issues
show
Bug introduced by
The method preSetHook() does not seem to exist on object<Pulsar\Model>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
870
            return false;
871
        }
872
873
        // validate the values being saved
874
        $validated = true;
875
        $updateArray = [];
876
        foreach ($this->_unsaved as $name => $value) {
877
            // exclude if value does not map to a property
878
            if (!isset(static::$properties[$name])) {
879
                continue;
880
            }
881
882
            $property = static::$properties[$name];
883
884
            // can only modify mutable properties
885
            if ($property['mutable'] != self::MUTABLE) {
886
                continue;
887
            }
888
889
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
890
            $updateArray[$name] = $value;
891
        }
892
893
        if (!$validated) {
894
            return false;
895
        }
896
897
        $updated = self::$driver->updateModel($this, $updateArray);
898
899
        if ($updated) {
900
            // NOTE clear the local cache before the model.updated
901
            // event so that fetching values forces a reload
902
            // from the storage layer
903
            $this->clearCache();
904
905
            // dispatch the model.updated event
906
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
907
                return false;
908
            }
909
        }
910
911
        return $updated;
912
    }
913
914
    /**
915
     * Delete the model.
916
     *
917
     * @return bool true when the operation was successful
918
     */
919
    public function delete()
920
    {
921
        if ($this->_id === false) {
922
            throw new BadMethodCallException('Can only call delete() on an existing model');
923
        }
924
925
        // dispatch the model.deleting event
926
        if (!$this->handleDispatch(ModelEvent::DELETING)) {
927
            return false;
928
        }
929
930
        $deleted = self::$driver->deleteModel($this);
931
932
        if ($deleted) {
933
            // dispatch the model.deleted event
934
            if (!$this->handleDispatch(ModelEvent::DELETED)) {
935
                return false;
936
            }
937
938
            $this->_persisted = false;
939
        }
940
941
        return $deleted;
942
    }
943
944
    /////////////////////////////
945
    // Queries
946
    /////////////////////////////
947
948
    /**
949
     * Generates a new query instance.
950
     *
951
     * @return Query
952
     */
953
    public static function query()
954
    {
955
        // Create a new model instance for the query to ensure
956
        // that the model's initialize() method gets called.
957
        // Otherwise, the property definitions will be incomplete.
958
        $model = new static();
959
960
        return new Query($model);
961
    }
962
963
    /**
964
     * Finds a single instance of a model given it's ID.
965
     *
966
     * @param mixed $id
967
     *
968
     * @return Model|false
969
     */
970
    public static function find($id)
971
    {
972
        $model = new static($id);
973
        $values = self::getDriver()->loadModel($model);
974
        if (!is_array($values)) {
975
            return false;
976
        }
977
978
        return $model->refreshWith($values);
979
    }
980
981
    /**
982
     * Finds a single instance of a model given it's ID or throws an exception.
983
     *
984
     * @param mixed $id
985
     *
986
     * @return Model
987
     *
988
     * @throws ModelNotFoundException when a model could not be found
989
     */
990
    public static function findOrFail($id)
991
    {
992
        $model = static::find($id);
993
        if (!$model) {
994
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
995
        }
996
997
        return $model;
998
    }
999
1000
    /**
1001
     * Gets the total number of records matching an optional criteria.
1002
     *
1003
     * @param array $where criteria
1004
     *
1005
     * @return int
1006
     */
1007
    public static function totalRecords(array $where = [])
1008
    {
1009
        $query = static::query();
1010
        $query->where($where);
1011
1012
        return self::getDriver()->totalRecords($query);
1013
    }
1014
1015
    /**
1016
     * @deprecated
1017
     *
1018
     * Checks if the model exists in the database
1019
     *
1020
     * @return bool
1021
     */
1022
    public function exists()
1023
    {
1024
        return static::totalRecords($this->ids()) == 1;
1025
    }
1026
1027
    /**
1028
     * Tells if this model instance has been persisted to the data layer.
1029
     *
1030
     * NOTE: this does not actually perform a check with the data layer
1031
     *
1032
     * @return bool
1033
     */
1034
    public function persisted()
1035
    {
1036
        return $this->_persisted;
1037
    }
1038
1039
    /**
1040
     * Loads the model from the storage layer.
1041
     *
1042
     * @return self
1043
     */
1044
    public function refresh()
1045
    {
1046
        if ($this->_id === false) {
1047
            return $this;
1048
        }
1049
1050
        $values = self::$driver->loadModel($this);
1051
1052
        if (!is_array($values)) {
1053
            return $this;
1054
        }
1055
1056
        // clear any relations
1057
        $this->_relationships = [];
1058
1059
        return $this->refreshWith($values);
1060
    }
1061
1062
    /**
1063
     * Loads values into the model.
1064
     *
1065
     * @param array $values values
1066
     *
1067
     * @return self
1068
     */
1069
    public function refreshWith(array $values)
1070
    {
1071
        $this->_persisted = true;
1072
        $this->_values = $values;
1073
1074
        return $this;
1075
    }
1076
1077
    /**
1078
     * Clears the cache for this model.
1079
     *
1080
     * @return self
1081
     */
1082
    public function clearCache()
1083
    {
1084
        $this->_unsaved = [];
1085
        $this->_values = [];
1086
        $this->_relationships = [];
1087
1088
        return $this;
1089
    }
1090
1091
    /////////////////////////////
1092
    // Relationships
1093
    /////////////////////////////
1094
1095
    /**
1096
     * @deprecated
1097
     *
1098
     * Gets the model object corresponding to a relation
1099
     * WARNING no check is used to see if the model returned actually exists
1100
     *
1101
     * @param string $propertyName property
1102
     *
1103
     * @return Model
1104
     */
1105
    public function relation($propertyName)
1106
    {
1107
        $property = static::getProperty($propertyName);
1108
1109
        if (!isset($this->_relationships[$propertyName])) {
1110
            $relationModelName = $property['relation'];
1111
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1112
        }
1113
1114
        return $this->_relationships[$propertyName];
1115
    }
1116
1117
    /**
1118
     * Creates the parent side of a One-To-One relationship.
1119
     *
1120
     * @param string $model      foreign model class
1121
     * @param string $foreignKey identifying key on foreign model
1122
     * @param string $localKey   identifying key on local model
1123
     *
1124
     * @return Relation\Relation
1125
     */
1126 View Code Duplication
    public function hasOne($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1127
    {
1128
        // the default local key would look like `user_id`
1129
        // for a model named User
1130
        if (!$foreignKey) {
1131
            $inflector = Inflector::get();
1132
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1133
        }
1134
1135
        if (!$localKey) {
1136
            $localKey = self::DEFAULT_ID_PROPERTY;
1137
        }
1138
1139
        return new HasOne($model, $foreignKey, $localKey, $this);
1140
    }
1141
1142
    /**
1143
     * Creates the child side of a One-To-One or One-To-Many relationship.
1144
     *
1145
     * @param string $model      foreign model class
1146
     * @param string $foreignKey identifying key on foreign model
1147
     * @param string $localKey   identifying key on local model
1148
     *
1149
     * @return Relation\Relation
1150
     */
1151 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...
1152
    {
1153
        if (!$foreignKey) {
1154
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1155
        }
1156
1157
        // the default local key would look like `user_id`
1158
        // for a model named User
1159
        if (!$localKey) {
1160
            $inflector = Inflector::get();
1161
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1162
        }
1163
1164
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1165
    }
1166
1167
    /**
1168
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1169
     *
1170
     * @param string $model      foreign model class
1171
     * @param string $foreignKey identifying key on foreign model
1172
     * @param string $localKey   identifying key on local model
1173
     *
1174
     * @return Relation\Relation
1175
     */
1176 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...
1177
    {
1178
        // the default local key would look like `user_id`
1179
        // for a model named User
1180
        if (!$foreignKey) {
1181
            $inflector = Inflector::get();
1182
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1183
        }
1184
1185
        if (!$localKey) {
1186
            $localKey = self::DEFAULT_ID_PROPERTY;
1187
        }
1188
1189
        return new HasMany($model, $foreignKey, $localKey, $this);
1190
    }
1191
1192
    /**
1193
     * Creates the child side of a Many-To-Many relationship.
1194
     *
1195
     * @param string $model      foreign model class
1196
     * @param string $foreignKey identifying key on foreign model
1197
     * @param string $localKey   identifying key on local model
1198
     *
1199
     * @return Relation\Relation
1200
     */
1201 View Code Duplication
    public function belongsToMany($model, $foreignKey = '', $localKey = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1202
    {
1203
        if (!$foreignKey) {
1204
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1205
        }
1206
1207
        // the default local key would look like `user_id`
1208
        // for a model named User
1209
        if (!$localKey) {
1210
            $inflector = Inflector::get();
1211
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1212
        }
1213
1214
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1215
    }
1216
1217
    /////////////////////////////
1218
    // Events
1219
    /////////////////////////////
1220
1221
    /**
1222
     * Gets the event dispatcher.
1223
     *
1224
     * @return EventDispatcher
1225
     */
1226
    public static function getDispatcher($ignoreCache = false)
1227
    {
1228
        $class = get_called_class();
1229
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1230
            self::$dispatchers[$class] = new EventDispatcher();
1231
        }
1232
1233
        return self::$dispatchers[$class];
1234
    }
1235
1236
    /**
1237
     * Subscribes to a listener to an event.
1238
     *
1239
     * @param string   $event    event name
1240
     * @param callable $listener
1241
     * @param int      $priority optional priority, higher #s get called first
1242
     */
1243
    public static function listen($event, callable $listener, $priority = 0)
1244
    {
1245
        static::getDispatcher()->addListener($event, $listener, $priority);
1246
    }
1247
1248
    /**
1249
     * Adds a listener to the model.creating and model.updating events.
1250
     *
1251
     * @param callable $listener
1252
     * @param int      $priority
1253
     */
1254
    public static function saving(callable $listener, $priority = 0)
1255
    {
1256
        static::listen(ModelEvent::CREATING, $listener, $priority);
1257
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1258
    }
1259
1260
    /**
1261
     * Adds a listener to the model.created and model.updated events.
1262
     *
1263
     * @param callable $listener
1264
     * @param int      $priority
1265
     */
1266
    public static function saved(callable $listener, $priority = 0)
1267
    {
1268
        static::listen(ModelEvent::CREATED, $listener, $priority);
1269
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1270
    }
1271
1272
    /**
1273
     * Adds a listener to the model.creating event.
1274
     *
1275
     * @param callable $listener
1276
     * @param int      $priority
1277
     */
1278
    public static function creating(callable $listener, $priority = 0)
1279
    {
1280
        static::listen(ModelEvent::CREATING, $listener, $priority);
1281
    }
1282
1283
    /**
1284
     * Adds a listener to the model.created event.
1285
     *
1286
     * @param callable $listener
1287
     * @param int      $priority
1288
     */
1289
    public static function created(callable $listener, $priority = 0)
1290
    {
1291
        static::listen(ModelEvent::CREATED, $listener, $priority);
1292
    }
1293
1294
    /**
1295
     * Adds a listener to the model.updating event.
1296
     *
1297
     * @param callable $listener
1298
     * @param int      $priority
1299
     */
1300
    public static function updating(callable $listener, $priority = 0)
1301
    {
1302
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1303
    }
1304
1305
    /**
1306
     * Adds a listener to the model.updated event.
1307
     *
1308
     * @param callable $listener
1309
     * @param int      $priority
1310
     */
1311
    public static function updated(callable $listener, $priority = 0)
1312
    {
1313
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1314
    }
1315
1316
    /**
1317
     * Adds a listener to the model.deleting event.
1318
     *
1319
     * @param callable $listener
1320
     * @param int      $priority
1321
     */
1322
    public static function deleting(callable $listener, $priority = 0)
1323
    {
1324
        static::listen(ModelEvent::DELETING, $listener, $priority);
1325
    }
1326
1327
    /**
1328
     * Adds a listener to the model.deleted event.
1329
     *
1330
     * @param callable $listener
1331
     * @param int      $priority
1332
     */
1333
    public static function deleted(callable $listener, $priority = 0)
1334
    {
1335
        static::listen(ModelEvent::DELETED, $listener, $priority);
1336
    }
1337
1338
    /**
1339
     * Dispatches an event.
1340
     *
1341
     * @param string $eventName
1342
     *
1343
     * @return ModelEvent
1344
     */
1345
    protected function dispatch($eventName)
1346
    {
1347
        $event = new ModelEvent($this);
1348
1349
        return static::getDispatcher()->dispatch($eventName, $event);
1350
    }
1351
1352
    /**
1353
     * Dispatches the given event and checks if it was successful.
1354
     *
1355
     * @param string $eventName
1356
     *
1357
     * @return bool true if the events were successfully propagated
1358
     */
1359
    private function handleDispatch($eventName)
1360
    {
1361
        $event = $this->dispatch($eventName);
1362
1363
        return !$event->isPropagationStopped();
1364
    }
1365
1366
    /////////////////////////////
1367
    // Validation
1368
    /////////////////////////////
1369
1370
    /**
1371
     * Validates and marshals a value to storage.
1372
     *
1373
     * @param array  $property
1374
     * @param string $propertyName
1375
     * @param mixed  $value
1376
     *
1377
     * @return bool
1378
     */
1379
    private function filterAndValidate(array $property, $propertyName, &$value)
1380
    {
1381
        // assume empty string is a null value for properties
1382
        // that are marked as optionally-null
1383
        if ($property['null'] && empty($value)) {
1384
            $value = null;
1385
1386
            return true;
1387
        }
1388
1389
        // validate
1390
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1391
1392
        // unique?
1393
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1394
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1395
        }
1396
1397
        return $valid;
1398
    }
1399
1400
    /**
1401
     * Validates a value for a property.
1402
     *
1403
     * @param array  $property
1404
     * @param string $propertyName
1405
     * @param mixed  $value
1406
     *
1407
     * @return array
1408
     */
1409
    private function validate(array $property, $propertyName, $value)
1410
    {
1411
        $valid = true;
1412
1413
        if (isset($property['validate']) && is_callable($property['validate'])) {
1414
            $valid = call_user_func_array($property['validate'], [$value]);
1415
        } elseif (isset($property['validate'])) {
1416
            $valid = Validate::is($value, $property['validate']);
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Validate::is() has been deprecated with message: Validates one or more fields based upon certain filters. Filters may be chained and will be executed in order
i.e. Validate::is( '[email protected]', 'email' ) or Validate::is( ['password1', 'password2'], 'matching|password:8|required' ). NOTE: some filters may modify the data, which is passed in by reference

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1417
        }
1418
1419
        if (!$valid) {
1420
            $this->app['errors']->push([
1421
                'error' => self::ERROR_VALIDATION_FAILED,
1422
                'params' => [
1423
                    'field' => $propertyName,
1424
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1425
        }
1426
1427
        return [$valid, $value];
1428
    }
1429
1430
    /**
1431
     * Checks if a value is unique for a property.
1432
     *
1433
     * @param array  $property
1434
     * @param string $propertyName
1435
     * @param mixed  $value
1436
     *
1437
     * @return bool
1438
     */
1439
    private function checkUniqueness(array $property, $propertyName, $value)
1440
    {
1441
        if (static::totalRecords([$propertyName => $value]) > 0) {
1442
            $this->app['errors']->push([
1443
                'error' => self::ERROR_NOT_UNIQUE,
1444
                'params' => [
1445
                    'field' => $propertyName,
1446
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1447
1448
            return false;
1449
        }
1450
1451
        return true;
1452
    }
1453
1454
    /**
1455
     * Gets the marshaled default value for a property (if set).
1456
     *
1457
     * @param string $property
1458
     *
1459
     * @return mixed
1460
     */
1461
    private function getPropertyDefault(array $property)
1462
    {
1463
        return array_value($property, 'default');
1464
    }
1465
}
1466