Completed
Push — master ( 7b98cc...63bdcf )
by Jared
02:21
created

Model::toArray()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 18
rs 9.4286
cc 3
eloc 8
nc 4
nop 0
1
<?php
2
3
/**
4
 * @package Pulsar
5
 * @author Jared King <[email protected]>
6
 * @link http://jaredtking.com
7
 * @copyright 2015 Jared King
8
 * @license MIT
9
 */
10
11
namespace Pulsar;
12
13
use BadMethodCallException;
14
use ICanBoogie\Inflector;
15
use InvalidArgumentException;
16
use Pulsar\Driver\DriverInterface;
17
use Pulsar\Relation\HasOne;
18
use Pulsar\Relation\BelongsTo;
19
use Pulsar\Relation\HasMany;
20
use Pulsar\Relation\BelongsToMany;
21
use Pimple\Container;
22
use Symfony\Component\EventDispatcher\EventDispatcher;
23
24
abstract class Model implements \ArrayAccess
25
{
26
    const IMMUTABLE = 0;
27
    const MUTABLE_CREATE_ONLY = 1;
28
    const MUTABLE = 2;
29
30
    const TYPE_STRING = 'string';
31
    const TYPE_NUMBER = 'number';
32
    const TYPE_BOOLEAN = 'boolean';
33
    const TYPE_DATE = 'date';
34
    const TYPE_OBJECT = 'object';
35
    const TYPE_ARRAY = 'array';
36
37
    const ERROR_REQUIRED_FIELD_MISSING = 'required_field_missing';
38
    const ERROR_VALIDATION_FAILED = 'validation_failed';
39
    const ERROR_NOT_UNIQUE = 'not_unique';
40
41
    const DEFAULT_ID_PROPERTY = 'id';
42
43
    /////////////////////////////
44
    // Model visible variables
45
    /////////////////////////////
46
47
    /**
48
     * List of model ID property names.
49
     *
50
     * @staticvar array
51
     */
52
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
53
54
    /**
55
     * Property definitions expressed as a key-value map with
56
     * property names as the keys.
57
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
58
     *
59
     * @staticvar array
60
     */
61
    protected static $properties = [];
62
63
    /**
64
     * @staticvar \Pimple\Container
65
     */
66
    protected static $injectedApp;
67
68
    /**
69
     * @staticvar array
70
     */
71
    protected static $dispatchers;
72
73
    /**
74
     * @var number|string|bool
75
     */
76
    protected $_id;
77
78
    /**
79
     * @var \Pimple\Container
80
     */
81
    protected $app;
82
83
    /**
84
     * @var array
85
     */
86
    protected $_values = [];
87
88
    /**
89
     * @var array
90
     */
91
    protected $_unsaved = [];
92
93
    /**
94
     * @var array
95
     */
96
    protected $_relationships = [];
97
98
    /////////////////////////////
99
    // Base model variables
100
    /////////////////////////////
101
102
    /**
103
     * @staticvar array
104
     */
105
    private static $propertyDefinitionBase = [
106
        'type' => self::TYPE_STRING,
107
        'mutable' => self::MUTABLE,
108
        'null' => false,
109
        'unique' => false,
110
        'required' => false,
111
    ];
112
113
    /**
114
     * @staticvar array
115
     */
116
    private static $defaultIDProperty = [
117
        'type' => self::TYPE_NUMBER,
118
        'mutable' => self::IMMUTABLE,
119
    ];
120
121
    /**
122
     * @staticvar array
123
     */
124
    private static $timestampProperties = [
125
        'created_at' => [
126
            'type' => self::TYPE_DATE,
127
            'default' => null,
128
            'null' => true,
129
            'validate' => 'timestamp|db_timestamp',
130
        ],
131
        'updated_at' => [
132
            'type' => self::TYPE_DATE,
133
            'validate' => 'timestamp|db_timestamp',
134
        ],
135
    ];
136
137
    /**
138
     * @staticvar array
139
     */
140
    private static $initialized = [];
141
142
    /**
143
     * @staticvar Model\Driver\DriverInterface
144
     */
145
    private static $driver;
146
147
    /**
148
     * @staticvar array
149
     */
150
    private static $accessors = [];
151
152
    /**
153
     * @staticvar array
154
     */
155
    private static $mutators = [];
156
157
    /**
158
     * @var bool
159
     */
160
    private $_ignoreUnsaved;
161
162
    /**
163
     * Creates a new model object.
164
     *
165
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
166
     * @param array                    $values optional key-value map to pre-seed model
167
     */
168
    public function __construct($id = false, array $values = [])
169
    {
170
        // initialize the model
171
        $this->app = self::$injectedApp;
172
        $this->init();
173
174
        // TODO need to store the id as an array
175
        // instead of a string to maintain type integrity
176
        if (is_array($id)) {
177
            // A model can be supplied as a primary key
178
            foreach ($id as &$el) {
179
                if ($el instanceof self) {
180
                    $el = $el->id();
181
                }
182
            }
183
184
            $id = implode(',', $id);
185
        // A model can be supplied as a primary key
186
        } elseif ($id instanceof self) {
187
            $id = $id->id();
188
        }
189
190
        $this->_id = $id;
191
192
        // load any given values
193
        if (count($values) > 0) {
194
            $this->refreshWith($values);
195
        }
196
    }
197
198
    /**
199
     * Performs initialization on this model.
200
     */
201
    private function init()
202
    {
203
        // ensure the initialize function is called only once
204
        $k = get_called_class();
205
        if (!isset(self::$initialized[$k])) {
206
            $this->initialize();
207
            self::$initialized[$k] = true;
208
        }
209
    }
210
211
    /**
212
     * The initialize() method is called once per model. It's used
213
     * to perform any one-off tasks before the model gets
214
     * constructed. This is a great place to add any model
215
     * properties. When extending this method be sure to call
216
     * parent::initialize() as some important stuff happens here.
217
     * If extending this method to add properties then you should
218
     * call parent::initialize() after adding any properties.
219
     */
220
    protected function initialize()
221
    {
222
        // load the driver
223
        static::getDriver();
0 ignored issues
show
Unused Code introduced by
The call to the method Pulsar\Model::getDriver() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
224
225
        // add in the default ID property
226
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
227
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
228
        }
229
230
        // add in the auto timestamp properties
231
        if (property_exists(get_called_class(), 'autoTimestamps')) {
232
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
233
        }
234
235
        // fill in each property by extending the property
236
        // definition base
237
        foreach (static::$properties as &$property) {
238
            $property = array_replace(self::$propertyDefinitionBase, $property);
239
        }
240
241
        // order the properties array by name for consistency
242
        // since it is constructed in a random order
243
        ksort(static::$properties);
244
    }
245
246
    /**
247
     * Injects a DI container.
248
     *
249
     * @param \Pimple\Container $app
250
     */
251
    public static function inject(Container $app)
252
    {
253
        self::$injectedApp = $app;
254
    }
255
256
    /**
257
     * Gets the DI container used for this model.
258
     *
259
     * @return Container
260
     */
261
    public function getApp()
262
    {
263
        return $this->app;
264
    }
265
266
    /**
267
     * Sets the driver for all models.
268
     *
269
     * @param Model\Driver\DriverInterface $driver
270
     */
271
    public static function setDriver(DriverInterface $driver)
272
    {
273
        self::$driver = $driver;
274
    }
275
276
    /**
277
     * Gets the driver for all models.
278
     *
279
     * @return Model\Driver\DriverInterface
280
     */
281
    public static function getDriver()
282
    {
283
        return self::$driver;
284
    }
285
286
    /**
287
     * Gets the name of the model without namespacing.
288
     *
289
     * @return string
290
     */
291
    public static function modelName()
292
    {
293
        $class_name = get_called_class();
294
295
        // strip namespacing
296
        $paths = explode('\\', $class_name);
297
298
        return end($paths);
299
    }
300
301
    /**
302
     * Gets the model ID.
303
     *
304
     * @return string|number|false ID
305
     */
306
    public function id()
307
    {
308
        return $this->_id;
309
    }
310
311
    /**
312
     * Gets a key-value map of the model ID.
313
     *
314
     * @return array ID map
315
     */
316
    public function ids()
317
    {
318
        $return = [];
319
320
        // match up id values from comma-separated id string with property names
321
        $ids = explode(',', $this->_id);
322
        $ids = array_reverse($ids);
323
324
        // TODO need to store the id as an array
325
        // instead of a string to maintain type integrity
326
        foreach (static::$ids as $k => $f) {
327
            $id = (count($ids) > 0) ? array_pop($ids) : false;
328
329
            $return[$f] = $id;
330
        }
331
332
        return $return;
333
    }
334
335
    /////////////////////////////
336
    // Magic Methods
337
    /////////////////////////////
338
339
    /**
340
     * Converts the model into a string.
341
     *
342
     * @return string
343
     */
344
    public function __toString()
345
    {
346
        return get_called_class().'('.$this->_id.')';
347
    }
348
349
    /**
350
     * Shortcut to a get() call for a given property.
351
     *
352
     * @param string $name
353
     *
354
     * @return mixed
355
     */
356
    public function __get($name)
357
    {
358
        $result = $this->get([$name]);
359
360
        return reset($result);
361
    }
362
363
    /**
364
     * Sets an unsaved value.
365
     *
366
     * @param string $name
367
     * @param mixed  $value
368
     */
369
    public function __set($name, $value)
370
    {
371
        // if changing property, remove relation model
372
        if (isset($this->_relationships[$name])) {
373
            unset($this->_relationships[$name]);
374
        }
375
376
        // call any mutators
377
        $mutator = self::getMutator($name);
378
        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...
379
            $this->_unsaved[$name] = $this->$mutator($value);
380
        } else {
381
            $this->_unsaved[$name] = $value;
382
        }
383
    }
384
385
    /**
386
     * Checks if an unsaved value or property exists by this name.
387
     *
388
     * @param string $name
389
     *
390
     * @return bool
391
     */
392
    public function __isset($name)
393
    {
394
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
395
    }
396
397
    /**
398
     * Unsets an unsaved value.
399
     *
400
     * @param string $name
401
     */
402
    public function __unset($name)
403
    {
404
        if (array_key_exists($name, $this->_unsaved)) {
405
            // if changing property, remove relation model
406
            if (isset($this->_relationships[$name])) {
407
                unset($this->_relationships[$name]);
408
            }
409
410
            unset($this->_unsaved[$name]);
411
        }
412
    }
413
414
    /////////////////////////////
415
    // ArrayAccess Interface
416
    /////////////////////////////
417
418
    public function offsetExists($offset)
419
    {
420
        return isset($this->$offset);
421
    }
422
423
    public function offsetGet($offset)
424
    {
425
        return $this->$offset;
426
    }
427
428
    public function offsetSet($offset, $value)
429
    {
430
        $this->$offset = $value;
431
    }
432
433
    public function offsetUnset($offset)
434
    {
435
        unset($this->$offset);
436
    }
437
438
    public static function __callStatic($name, $parameters)
439
    {
440
        // Any calls to unkown static methods should be deferred to
441
        // the query. This allows calls like User::where()
442
        // 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...
443
        return call_user_func_array([static::query(), $name], $parameters);
444
    }
445
446
    /////////////////////////////
447
    // Property Definitions
448
    /////////////////////////////
449
450
    /**
451
     * Gets all the property definitions for the model.
452
     *
453
     * @return array key-value map of properties
454
     */
455
    public static function getProperties()
456
    {
457
        return static::$properties;
458
    }
459
460
    /**
461
     * Gets a property defition for the model.
462
     *
463
     * @param string $property property to lookup
464
     *
465
     * @return array|null property
466
     */
467
    public static function getProperty($property)
468
    {
469
        return array_value(static::$properties, $property);
470
    }
471
472
    /**
473
     * Gets the names of the model ID properties.
474
     *
475
     * @return array
476
     */
477
    public static function getIDProperties()
478
    {
479
        return static::$ids;
480
    }
481
482
    /**
483
     * Checks if the model has a property.
484
     *
485
     * @param string $property property
486
     *
487
     * @return bool has property
488
     */
489
    public static function hasProperty($property)
490
    {
491
        return isset(static::$properties[$property]);
492
    }
493
494
    /**
495
     * Gets the mutator method name for a given proeprty name.
496
     * Looks for methods in the form of `setPropertyValue`.
497
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
498
     *
499
     * @param string $property property
500
     *
501
     * @return string|false method name if it exists
502
     */
503 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...
504
    {
505
        $class = get_called_class();
506
507
        $k = $class.':'.$property;
508
        if (!array_key_exists($k, self::$mutators)) {
509
            $inflector = Inflector::get();
510
            $method = 'set'.$inflector->camelize($property).'Value';
511
512
            if (!method_exists($class, $method)) {
513
                $method = false;
514
            }
515
516
            self::$mutators[$k] = $method;
517
        }
518
519
        return self::$mutators[$k];
520
    }
521
522
    /**
523
     * Gets the accessor method name for a given proeprty name.
524
     * Looks for methods in the form of `getPropertyValue`.
525
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
526
     *
527
     * @param string $property property
528
     *
529
     * @return string|false method name if it exists
530
     */
531 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...
532
    {
533
        $class = get_called_class();
534
535
        $k = $class.':'.$property;
536
        if (!array_key_exists($k, self::$accessors)) {
537
            $inflector = Inflector::get();
538
            $method = 'get'.$inflector->camelize($property).'Value';
539
540
            if (!method_exists($class, $method)) {
541
                $method = false;
542
            }
543
544
            self::$accessors[$k] = $method;
545
        }
546
547
        return self::$accessors[$k];
548
    }
549
550
    /////////////////////////////
551
    // CRUD Operations
552
    /////////////////////////////
553
554
    /**
555
     * Saves the model.
556
     *
557
     * @return bool
558
     */
559
    public function save()
560
    {
561
        if ($this->_id === false) {
562
            return $this->create();
563
        }
564
565
        return $this->set($this->_unsaved);
566
    }
567
568
    /**
569
     * Creates a new model.
570
     *
571
     * @param array $data optional key-value properties to set
572
     *
573
     * @return bool
574
     *
575
     * @throws BadMethodCallException when called on an existing model
576
     */
577
    public function create(array $data = [])
578
    {
579
        if ($this->_id !== false) {
580
            throw new BadMethodCallException('Cannot call create() on an existing model');
581
        }
582
583
        if (!empty($data)) {
584
            foreach ($data as $k => $value) {
585
                $this->$k = $value;
586
            }
587
        }
588
589
        // dispatch the model.creating event
590
        if (!$this->handleDispatch(ModelEvent::CREATING)) {
591
            return false;
592
        }
593
594
        $requiredProperties = [];
595
        foreach (static::$properties as $name => $property) {
596
            // build a list of the required properties
597
            if ($property['required']) {
598
                $requiredProperties[] = $name;
599
            }
600
601
            // add in default values
602
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
603
                $this->_unsaved[$name] = $property['default'];
604
            }
605
        }
606
607
        // validate the values being saved
608
        $validated = true;
609
        $insertArray = [];
610
        foreach ($this->_unsaved as $name => $value) {
611
            // exclude if value does not map to a property
612
            if (!isset(static::$properties[$name])) {
613
                continue;
614
            }
615
616
            $property = static::$properties[$name];
617
618
            // cannot insert immutable values
619
            // (unless using the default value)
620
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
621
                continue;
622
            }
623
624
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
625
            $insertArray[$name] = $value;
626
        }
627
628
        // check for required fields
629
        foreach ($requiredProperties as $name) {
630
            if (!isset($insertArray[$name])) {
631
                $property = static::$properties[$name];
632
                $this->app['errors']->push([
633
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
634
                    'params' => [
635
                        'field' => $name,
636
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
637
638
                $validated = false;
639
            }
640
        }
641
642
        if (!$validated) {
643
            return false;
644
        }
645
646
        $created = self::$driver->createModel($this, $insertArray);
647
648
        if ($created) {
649
            // determine the model's new ID
650
            $this->_id = $this->getNewID();
651
652
            // NOTE clear the local cache before the model.created
653
            // event so that fetching values forces a reload
654
            // from the storage layer
655
            $this->clearCache();
656
657
            // dispatch the model.created event
658
            if (!$this->handleDispatch(ModelEvent::CREATED)) {
659
                return false;
660
            }
661
        }
662
663
        return $created;
664
    }
665
666
    /**
667
     * Ignores unsaved values when fetching the next value.
668
     *
669
     * @return self
670
     */
671
    public function ignoreUnsaved()
672
    {
673
        $this->_ignoreUnsaved = true;
674
675
        return $this;
676
    }
677
678
    /**
679
     * Fetches property values from the model.
680
     *
681
     * This method looks up values in this order:
682
     * IDs, local cache, unsaved values, storage layer, defaults
683
     *
684
     * @param array $properties list of property names to fetch values of
685
     *
686
     * @return array
687
     */
688
    public function get(array $properties)
689
    {
690
        // load the values from the IDs and local model cache
691
        $values = array_replace($this->ids(), $this->_values);
692
693
        // unless specified, use any unsaved values
694
        $ignoreUnsaved = $this->_ignoreUnsaved;
695
        $this->_ignoreUnsaved = false;
696
697
        if (!$ignoreUnsaved) {
698
            $values = array_replace($values, $this->_unsaved);
699
        }
700
701
        // attempt to load any missing values from the storage layer
702
        $missing = array_diff($properties, array_keys($values));
703
        if (count($missing) > 0) {
704
            // load values for the model
705
            $this->refresh();
706
            $values = array_replace($values, $this->_values);
707
708
            // add back any unsaved values
709
            if (!$ignoreUnsaved) {
710
                $values = array_replace($values, $this->_unsaved);
711
            }
712
        }
713
714
        return $this->buildGetResponse($properties, $values);
715
    }
716
717
    /**
718
     * Builds a key-value map of the requested properties given a set of values
719
     *
720
     * @param array $properties
721
     * @param array $values
722
     *
723
     * @return array
724
     *
725
     * @throws InvalidArgumentException when a property was requested not present in the values
726
     */
727
    private function buildGetResponse(array $properties, array $values)
728
    {
729
        $response = [];
730
        foreach ($properties as $k) {
731
            $accessor = self::getAccessor($k);
732
733
            // use the supplied value if it's available
734
            if (array_key_exists($k, $values)) {
735
                $response[$k] = $values[$k];
736
            // set any missing values to the default value
737
            } elseif (static::hasProperty($k)) {
738
                $response[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
739
            // throw an exception for non-properties that do not
740
            // have an accessor
741
            } else if (!$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...
742
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
743
            // otherwise the value is considered null
744
            } else {
745
                $response[$k] = null;
746
            }
747
748
            // call any accessors
749
            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...
750
                $response[$k] = $this->$accessor($response[$k]);
751
            }
752
        }
753
754
        return $response;
755
    }
756
757
    /**
758
     * Gets the ID for a newly created model.
759
     *
760
     * @return string
761
     */
762
    protected function getNewID()
763
    {
764
        $ids = [];
765
        foreach (static::$ids as $k) {
766
            // attempt use the supplied value if the ID property is mutable
767
            $property = static::getProperty($k);
768
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
769
                $ids[] = $this->_unsaved[$k];
770
            } else {
771
                $ids[] = self::$driver->getCreatedID($this, $k);
772
            }
773
        }
774
775
        // TODO need to store the id as an array
776
        // instead of a string to maintain type integrity
777
        return (count($ids) > 1) ? implode(',', $ids) : $ids[0];
778
    }
779
780
    /**
781
     * Converts the model to an array.
782
     *
783
     * @return array model array
784
     */
785
    public function toArray()
786
    {
787
        // build the list of properties to retrieve
788
        $properties = array_keys(static::$properties);
789
790
        // remove any hidden properties
791
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
792
        $properties = array_diff($properties, $hide);
793
794
        // add any appended properties
795
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
796
        $properties = array_merge($properties, $append);
797
798
        // get the values for the properties
799
        $result = $this->get($properties);
800
801
        return $result;
802
    }
803
804
    /**
805
     * Updates the model.
806
     *
807
     * @param array $data optional key-value properties to set
808
     *
809
     * @return bool
810
     *
811
     * @throws BadMethodCallException when not called on an existing model
812
     */
813
    public function set(array $data = [])
814
    {
815
        if ($this->_id === false) {
816
            throw new BadMethodCallException('Can only call set() on an existing model');
817
        }
818
819
        // not updating anything?
820
        if (count($data) == 0) {
821
            return true;
822
        }
823
824
        // apply mutators
825
        foreach ($data as $k => $value) {
826
            if ($mutator = self::getMutator($k)) {
827
                $data[$k] = $this->$mutator($value);
828
            }
829
        }
830
831
        // dispatch the model.updating event
832
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
833
            return false;
834
        }
835
836
        // validate the values being saved
837
        $validated = true;
838
        $updateArray = [];
839
        foreach ($data as $name => $value) {
840
            // exclude if value does not map to a property
841
            if (!isset(static::$properties[$name])) {
842
                continue;
843
            }
844
845
            $property = static::$properties[$name];
846
847
            // can only modify mutable properties
848
            if ($property['mutable'] != self::MUTABLE) {
849
                continue;
850
            }
851
852
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
853
            $updateArray[$name] = $value;
854
        }
855
856
        if (!$validated) {
857
            return false;
858
        }
859
860
        $updated = self::$driver->updateModel($this, $updateArray);
861
862
        if ($updated) {
863
            // NOTE clear the local cache before the model.updated
864
            // event so that fetching values forces a reload
865
            // from the storage layer
866
            $this->clearCache();
867
868
            // dispatch the model.updated event
869
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
870
                return false;
871
            }
872
        }
873
874
        return $updated;
875
    }
876
877
    /**
878
     * Delete the model.
879
     *
880
     * @return bool success
881
     */
882
    public function delete()
883
    {
884
        if ($this->_id === false) {
885
            throw new BadMethodCallException('Can only call delete() on an existing model');
886
        }
887
888
        // dispatch the model.deleting event
889
        if (!$this->handleDispatch(ModelEvent::DELETING)) {
890
            return false;
891
        }
892
893
        $deleted = self::$driver->deleteModel($this);
894
895
        if ($deleted) {
896
            // dispatch the model.deleted event
897
            if (!$this->handleDispatch(ModelEvent::DELETED)) {
898
                return false;
899
            }
900
901
            // NOTE clear the local cache before the model.deleted
902
            // event so that fetching values forces a reload
903
            // from the storage layer
904
            $this->clearCache();
905
        }
906
907
        return $deleted;
908
    }
909
910
    /////////////////////////////
911
    // Queries
912
    /////////////////////////////
913
914
    /**
915
     * Generates a new query instance.
916
     *
917
     * @return Model\Query
918
     */
919
    public static function query()
920
    {
921
        // Create a new model instance for the query to ensure
922
        // that the model's initialize() method gets called.
923
        // Otherwise, the property definitions will be incomplete.
924
        $model = new static();
925
926
        return new Query($model);
927
    }
928
929
    /**
930
     * Gets the toal number of records matching an optional criteria.
931
     *
932
     * @param array $where criteria
933
     *
934
     * @return int total
935
     */
936
    public static function totalRecords(array $where = [])
937
    {
938
        $query = static::query();
939
        $query->where($where);
940
941
        return self::getDriver()->totalRecords($query);
942
    }
943
944
    /**
945
     * Checks if the model exists in the database.
946
     *
947
     * @return bool
948
     */
949
    public function exists()
950
    {
951
        return static::totalRecords($this->ids()) == 1;
952
    }
953
954
    /**
955
     * @deprecated alias for refresh()
956
     */
957
    public function load()
958
    {
959
        return $this->refresh();
960
    }
961
962
    /**
963
     * Loads the model from the storage layer.
964
     *
965
     * @return self
966
     */
967
    public function refresh()
968
    {
969
        if ($this->_id === false) {
970
            return $this;
971
        }
972
973
        $values = self::$driver->loadModel($this);
974
975
        if (!is_array($values)) {
976
            return $this;
977
        }
978
979
        // clear any relations
980
        $this->_relationships = [];
981
982
        return $this->refreshWith($values);
983
    }
984
985
    /**
986
     * Loads values into the model.
987
     *
988
     * @param array $values values
989
     *
990
     * @return self
991
     */
992
    public function refreshWith(array $values)
993
    {
994
        $this->_values = $values;
995
996
        return $this;
997
    }
998
999
    /**
1000
     * Clears the cache for this model.
1001
     *
1002
     * @return self
1003
     */
1004
    public function clearCache()
1005
    {
1006
        $this->_unsaved = [];
1007
        $this->_values = [];
1008
        $this->_relationships = [];
1009
1010
        return $this;
1011
    }
1012
1013
    /////////////////////////////
1014
    // Relationships
1015
    /////////////////////////////
1016
1017
    /**
1018
     * Gets the model object corresponding to a relation
1019
     * WARNING no check is used to see if the model returned actually exists.
1020
     *
1021
     * @param string $propertyName property
1022
     *
1023
     * @return \Pulsar\Model model
1024
     */
1025
    public function relation($propertyName)
1026
    {
1027
        // TODO deprecated
1028
        $property = static::getProperty($propertyName);
1029
1030
        if (!isset($this->_relationships[$propertyName])) {
1031
            $relationModelName = $property['relation'];
1032
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1033
        }
1034
1035
        return $this->_relationships[$propertyName];
1036
    }
1037
1038
    /**
1039
     * Creates the parent side of a One-To-One relationship.
1040
     *
1041
     * @param string $model      foreign model class
1042
     * @param string $foreignKey identifying key on foreign model
1043
     * @param string $localKey   identifying key on local model
1044
     *
1045
     * @return Relation
1046
     */
1047 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...
1048
    {
1049
        // the default local key would look like `user_id`
1050
        // for a model named User
1051
        if (!$foreignKey) {
1052
            $inflector = Inflector::get();
1053
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1054
        }
1055
1056
        if (!$localKey) {
1057
            $localKey = self::DEFAULT_ID_PROPERTY;
1058
        }
1059
1060
        return new HasOne($model, $foreignKey, $localKey, $this);
1061
    }
1062
1063
    /**
1064
     * Creates the child side of a One-To-One or One-To-Many relationship.
1065
     *
1066
     * @param string $model      foreign model class
1067
     * @param string $foreignKey identifying key on foreign model
1068
     * @param string $localKey   identifying key on local model
1069
     *
1070
     * @return Relation
1071
     */
1072 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...
1073
    {
1074
        if (!$foreignKey) {
1075
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1076
        }
1077
1078
        // the default local key would look like `user_id`
1079
        // for a model named User
1080
        if (!$localKey) {
1081
            $inflector = Inflector::get();
1082
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1083
        }
1084
1085
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1086
    }
1087
1088
    /**
1089
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1090
     *
1091
     * @param string $model      foreign model class
1092
     * @param string $foreignKey identifying key on foreign model
1093
     * @param string $localKey   identifying key on local model
1094
     *
1095
     * @return Relation
1096
     */
1097 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...
1098
    {
1099
        // the default local key would look like `user_id`
1100
        // for a model named User
1101
        if (!$foreignKey) {
1102
            $inflector = Inflector::get();
1103
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1104
        }
1105
1106
        if (!$localKey) {
1107
            $localKey = self::DEFAULT_ID_PROPERTY;
1108
        }
1109
1110
        return new HasMany($model, $foreignKey, $localKey, $this);
1111
    }
1112
1113
    /**
1114
     * Creates the child side of a Many-To-Many relationship.
1115
     *
1116
     * @param string $model      foreign model class
1117
     * @param string $foreignKey identifying key on foreign model
1118
     * @param string $localKey   identifying key on local model
1119
     *
1120
     * @return Relation
1121
     */
1122 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...
1123
    {
1124
        if (!$foreignKey) {
1125
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1126
        }
1127
1128
        // the default local key would look like `user_id`
1129
        // for a model named User
1130
        if (!$localKey) {
1131
            $inflector = Inflector::get();
1132
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1133
        }
1134
1135
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1136
    }
1137
1138
    /////////////////////////////
1139
    // Events
1140
    /////////////////////////////
1141
1142
    /**
1143
     * Gets the event dispatcher.
1144
     *
1145
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1146
     */
1147
    public static function getDispatcher($ignoreCache = false)
1148
    {
1149
        $class = get_called_class();
1150
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1151
            self::$dispatchers[$class] = new EventDispatcher();
1152
        }
1153
1154
        return self::$dispatchers[$class];
1155
    }
1156
1157
    /**
1158
     * Subscribes to a listener to an event.
1159
     *
1160
     * @param string   $event    event name
1161
     * @param callable $listener
1162
     * @param int      $priority optional priority, higher #s get called first
1163
     */
1164
    public static function listen($event, callable $listener, $priority = 0)
1165
    {
1166
        static::getDispatcher()->addListener($event, $listener, $priority);
1167
    }
1168
1169
    /**
1170
     * Adds a listener to the model.creating event.
1171
     *
1172
     * @param callable $listener
1173
     * @param int      $priority
1174
     */
1175
    public static function creating(callable $listener, $priority = 0)
1176
    {
1177
        static::listen(ModelEvent::CREATING, $listener, $priority);
1178
    }
1179
1180
    /**
1181
     * Adds a listener to the model.created event.
1182
     *
1183
     * @param callable $listener
1184
     * @param int      $priority
1185
     */
1186
    public static function created(callable $listener, $priority = 0)
1187
    {
1188
        static::listen(ModelEvent::CREATED, $listener, $priority);
1189
    }
1190
1191
    /**
1192
     * Adds a listener to the model.updating event.
1193
     *
1194
     * @param callable $listener
1195
     * @param int      $priority
1196
     */
1197
    public static function updating(callable $listener, $priority = 0)
1198
    {
1199
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1200
    }
1201
1202
    /**
1203
     * Adds a listener to the model.updated event.
1204
     *
1205
     * @param callable $listener
1206
     * @param int      $priority
1207
     */
1208
    public static function updated(callable $listener, $priority = 0)
1209
    {
1210
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1211
    }
1212
1213
    /**
1214
     * Adds a listener to the model.deleting event.
1215
     *
1216
     * @param callable $listener
1217
     * @param int      $priority
1218
     */
1219
    public static function deleting(callable $listener, $priority = 0)
1220
    {
1221
        static::listen(ModelEvent::DELETING, $listener, $priority);
1222
    }
1223
1224
    /**
1225
     * Adds a listener to the model.deleted event.
1226
     *
1227
     * @param callable $listener
1228
     * @param int      $priority
1229
     */
1230
    public static function deleted(callable $listener, $priority = 0)
1231
    {
1232
        static::listen(ModelEvent::DELETED, $listener, $priority);
1233
    }
1234
1235
    /**
1236
     * Dispatches an event.
1237
     *
1238
     * @param string $eventName
1239
     *
1240
     * @return Model\ModelEvent
1241
     */
1242
    protected function dispatch($eventName)
1243
    {
1244
        $event = new ModelEvent($this);
1245
1246
        return static::getDispatcher()->dispatch($eventName, $event);
1247
    }
1248
1249
    /**
1250
     * Dispatches the given event and checks if it was successful.
1251
     *
1252
     * @param string $eventName
1253
     *
1254
     * @return bool
1255
     */
1256
    private function handleDispatch($eventName)
1257
    {
1258
        $event = $this->dispatch($eventName);
1259
        
1260
        return !$event->isPropagationStopped();
1261
    }
1262
1263
    /////////////////////////////
1264
    // Validation
1265
    /////////////////////////////
1266
1267
    /**
1268
     * Validates and marshals a value to storage.
1269
     *
1270
     * @param array  $property
1271
     * @param string $propertyName
1272
     * @param mixed  $value
1273
     *
1274
     * @return bool
1275
     */
1276
    private function filterAndValidate(array $property, $propertyName, &$value)
1277
    {
1278
        // assume empty string is a null value for properties
1279
        // that are marked as optionally-null
1280
        if ($property['null'] && empty($value)) {
1281
            $value = null;
1282
1283
            return true;
1284
        }
1285
1286
        // validate
1287
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1288
1289
        // unique?
1290
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1291
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1292
        }
1293
1294
        return $valid;
1295
    }
1296
1297
    /**
1298
     * Validates a value for a property.
1299
     *
1300
     * @param array  $property
1301
     * @param string $propertyName
1302
     * @param mixed  $value
1303
     *
1304
     * @return bool
1305
     */
1306
    private function validate(array $property, $propertyName, $value)
1307
    {
1308
        $valid = true;
1309
1310
        if (isset($property['validate']) && is_callable($property['validate'])) {
1311
            $valid = call_user_func_array($property['validate'], [$value]);
1312
        } elseif (isset($property['validate'])) {
1313
            $valid = Validate::is($value, $property['validate']);
1314
        }
1315
1316
        if (!$valid) {
1317
            $this->app['errors']->push([
1318
                'error' => self::ERROR_VALIDATION_FAILED,
1319
                'params' => [
1320
                    'field' => $propertyName,
1321
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1322
        }
1323
1324
        return [$valid, $value];
1325
    }
1326
1327
    /**
1328
     * Checks if a value is unique for a property.
1329
     *
1330
     * @param array  $property
1331
     * @param string $propertyName
1332
     * @param mixed  $value
1333
     *
1334
     * @return bool
1335
     */
1336
    private function checkUniqueness(array $property, $propertyName, $value)
1337
    {
1338
        if (static::totalRecords([$propertyName => $value]) > 0) {
1339
            $this->app['errors']->push([
1340
                'error' => self::ERROR_NOT_UNIQUE,
1341
                'params' => [
1342
                    'field' => $propertyName,
1343
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1344
1345
            return false;
1346
        }
1347
1348
        return true;
1349
    }
1350
1351
    /**
1352
     * Gets the marshaled default value for a property (if set).
1353
     *
1354
     * @param string $property
1355
     *
1356
     * @return mixed
1357
     */
1358
    private function getPropertyDefault(array $property)
1359
    {
1360
        return array_value($property, 'default');
1361
    }
1362
}
1363