Completed
Push — master ( aa571b...f96361 )
by Jared
02:42
created

Model::loadRelationship()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6667
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @link http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
namespace Pulsar;
12
13
use BadMethodCallException;
14
use ICanBoogie\Inflector;
15
use Infuse\ErrorStack;
16
use InvalidArgumentException;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Relation\HasOne;
19
use Pulsar\Relation\BelongsTo;
20
use Pulsar\Relation\HasMany;
21
use Pulsar\Relation\BelongsToMany;
22
use Pimple\Container;
23
use Symfony\Component\EventDispatcher\EventDispatcher;
24
25
abstract class Model implements \ArrayAccess
26
{
27
    const IMMUTABLE = 0;
28
    const MUTABLE_CREATE_ONLY = 1;
29
    const MUTABLE = 2;
30
31
    const TYPE_STRING = 'string';
32
    const TYPE_NUMBER = 'number';
33
    const TYPE_BOOLEAN = 'boolean';
34
    const TYPE_DATE = 'date';
35
    const TYPE_OBJECT = 'object';
36
    const TYPE_ARRAY = 'array';
37
38
    const ERROR_REQUIRED_FIELD_MISSING = 'required_field_missing';
39
    const ERROR_VALIDATION_FAILED = 'validation_failed';
40
    const ERROR_NOT_UNIQUE = 'not_unique';
41
42
    const DEFAULT_ID_PROPERTY = 'id';
43
44
    /////////////////////////////
45
    // Model visible variables
46
    /////////////////////////////
47
48
    /**
49
     * List of model ID property names.
50
     *
51
     * @staticvar array
52
     */
53
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
54
55
    /**
56
     * Property definitions expressed as a key-value map with
57
     * property names as the keys.
58
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
59
     *
60
     * @staticvar array
61
     */
62
    protected static $properties = [];
63
64
    /**
65
     * @staticvar array
66
     */
67
    protected static $relationships = [];
68
69
    /**
70
     * @staticvar \Pimple\Container
71
     */
72
    protected static $injectedApp;
73
74
    /**
75
     * @staticvar array
76
     */
77
    protected static $dispatchers;
78
79
    /**
80
     * @var number|string|bool
81
     */
82
    protected $_id;
83
84
    /**
85
     * @var \Pimple\Container
86
     */
87
    protected $app;
88
89
    /**
90
     * @var array
91
     */
92
    protected $_values = [];
93
94
    /**
95
     * @var array
96
     */
97
    protected $_unsaved = [];
98
99
    /**
100
     * @var \Infuse\ErrorStack
101
     */
102
    protected $_errors;
103
104
    /////////////////////////////
105
    // Base model variables
106
    /////////////////////////////
107
108
    /**
109
     * @staticvar array
110
     */
111
    private static $propertyDefinitionBase = [
112
        'type' => self::TYPE_STRING,
113
        'mutable' => self::MUTABLE,
114
        'null' => false,
115
        'unique' => false,
116
        'required' => false,
117
    ];
118
119
    /**
120
     * @staticvar array
121
     */
122
    private static $defaultIDProperty = [
123
        'type' => self::TYPE_NUMBER,
124
        'mutable' => self::IMMUTABLE,
125
    ];
126
127
    /**
128
     * @staticvar array
129
     */
130
    private static $timestampProperties = [
131
        'created_at' => [
132
            'type' => self::TYPE_DATE,
133
            'default' => null,
134
            'null' => true,
135
            'validate' => 'timestamp|db_timestamp',
136
        ],
137
        'updated_at' => [
138
            'type' => self::TYPE_DATE,
139
            'validate' => 'timestamp|db_timestamp',
140
        ],
141
    ];
142
143
    /**
144
     * @staticvar array
145
     */
146
    private static $initialized = [];
147
148
    /**
149
     * @staticvar Model\Driver\DriverInterface
150
     */
151
    private static $driver;
152
153
    /**
154
     * @staticvar array
155
     */
156
    private static $accessors = [];
157
158
    /**
159
     * @staticvar array
160
     */
161
    private static $mutators = [];
162
163
    /**
164
     * @var bool
165
     */
166
    private $_ignoreUnsaved;
167
168
    /**
169
     * Creates a new model object.
170
     *
171
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
172
     * @param array                    $values optional key-value map to pre-seed model
173
     */
174
    public function __construct($id = false, array $values = [])
175
    {
176
        // initialize the model
177
        $this->app = self::$injectedApp;
178
        $this->init();
179
180
        // TODO need to store the id as an array
181
        // instead of a string to maintain type integrity
182
        if (is_array($id)) {
183
            // A model can be supplied as a primary key
184
            foreach ($id as &$el) {
185
                if ($el instanceof self) {
186
                    $el = $el->id();
187
                }
188
            }
189
190
            $id = implode(',', $id);
191
        // A model can be supplied as a primary key
192
        } elseif ($id instanceof self) {
193
            $id = $id->id();
194
        }
195
196
        $this->_id = $id;
197
198
        // load any given values
199
        if (count($values) > 0) {
200
            $this->refreshWith($values);
201
        }
202
    }
203
204
    /**
205
     * Performs initialization on this model.
206
     */
207
    private function init()
208
    {
209
        // ensure the initialize function is called only once
210
        $k = get_called_class();
211
        if (!isset(self::$initialized[$k])) {
212
            $this->initialize();
213
            self::$initialized[$k] = true;
214
        }
215
    }
216
217
    /**
218
     * The initialize() method is called once per model. It's used
219
     * to perform any one-off tasks before the model gets
220
     * constructed. This is a great place to add any model
221
     * properties. When extending this method be sure to call
222
     * parent::initialize() as some important stuff happens here.
223
     * If extending this method to add properties then you should
224
     * call parent::initialize() after adding any properties.
225
     */
226
    protected function initialize()
227
    {
228
        // load the driver
229
        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...
230
231
        // add in the default ID property
232
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
233
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
234
        }
235
236
        // add in the auto timestamp properties
237
        if (property_exists(get_called_class(), 'autoTimestamps')) {
238
            static::$properties = array_replace(self::$timestampProperties, static::$properties);
239
        }
240
241
        // fill in each property by extending the property
242
        // definition base
243
        foreach (static::$properties as &$property) {
244
            $property = array_replace(self::$propertyDefinitionBase, $property);
245
        }
246
247
        // order the properties array by name for consistency
248
        // since it is constructed in a random order
249
        ksort(static::$properties);
250
    }
251
252
    /**
253
     * Injects a DI container.
254
     *
255
     * @param \Pimple\Container $app
256
     */
257
    public static function inject(Container $app)
258
    {
259
        self::$injectedApp = $app;
260
    }
261
262
    /**
263
     * Gets the DI container used for this model.
264
     *
265
     * @return \Pimple\Container
266
     */
267
    public function getApp()
268
    {
269
        return $this->app;
270
    }
271
272
    /**
273
     * Sets the driver for all models.
274
     *
275
     * @param Model\Driver\DriverInterface $driver
276
     */
277
    public static function setDriver(DriverInterface $driver)
278
    {
279
        self::$driver = $driver;
280
    }
281
282
    /**
283
     * Gets the driver for all models.
284
     *
285
     * @return Model\Driver\DriverInterface
286
     */
287
    public static function getDriver()
288
    {
289
        return self::$driver;
290
    }
291
292
    /**
293
     * Gets the name of the model without namespacing.
294
     *
295
     * @return string
296
     */
297
    public static function modelName()
298
    {
299
        $class_name = get_called_class();
300
301
        // strip namespacing
302
        $paths = explode('\\', $class_name);
303
304
        return end($paths);
305
    }
306
307
    /**
308
     * Gets the model ID.
309
     *
310
     * @return string|number|false ID
311
     */
312
    public function id()
313
    {
314
        return $this->_id;
315
    }
316
317
    /**
318
     * Gets a key-value map of the model ID.
319
     *
320
     * @return array ID map
321
     */
322
    public function ids()
323
    {
324
        $return = [];
325
326
        // match up id values from comma-separated id string with property names
327
        $ids = explode(',', $this->_id);
328
        $ids = array_reverse($ids);
329
330
        // TODO need to store the id as an array
331
        // instead of a string to maintain type integrity
332
        foreach (static::$ids as $k => $f) {
333
            $id = (count($ids) > 0) ? array_pop($ids) : false;
334
335
            $return[$f] = $id;
336
        }
337
338
        return $return;
339
    }
340
341
    /////////////////////////////
342
    // Magic Methods
343
    /////////////////////////////
344
345
    /**
346
     * Converts the model into a string.
347
     *
348
     * @return string
349
     */
350
    public function __toString()
351
    {
352
        return get_called_class().'('.$this->_id.')';
353
    }
354
355
    /**
356
     * Shortcut to a get() call for a given property.
357
     *
358
     * @param string $name
359
     *
360
     * @return mixed
361
     */
362
    public function __get($name)
363
    {
364
        // get relationship values
365
        if (static::isRelationship($name)) {
366
            return $this->loadRelationship($name);
367
        }
368
369
        // get property values
370
        $result = $this->get([$name]);
371
372
        return reset($result);
373
    }
374
375
    /**
376
     * Sets an unsaved value.
377
     *
378
     * @param string $name
379
     * @param mixed  $value
380
     *
381
     * @throws BadMethodCallException
382
     */
383
    public function __set($name, $value)
384
    {
385
        if (static::isRelationship($name)) {
386
            throw new BadMethodCallException("Cannot set the `$name` property because it is a relationship");
387
        }
388
389
        // call any mutators
390
        $mutator = self::getMutator($name);
391
        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...
392
            $this->_unsaved[$name] = $this->$mutator($value);
393
        } else {
394
            $this->_unsaved[$name] = $value;
395
        }
396
    }
397
398
    /**
399
     * Checks if an unsaved value or property exists by this name.
400
     *
401
     * @param string $name
402
     *
403
     * @return bool
404
     */
405
    public function __isset($name)
406
    {
407
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
408
    }
409
410
    /**
411
     * Unsets an unsaved value.
412
     *
413
     * @param string $name
414
     *
415
     * @throws BadMethodCallException
416
     */
417
    public function __unset($name)
418
    {
419
        if (static::isRelationship($name)) {
420
            throw new BadMethodCallException("Cannot unset the `$name` property because it is a relationship");
421
        }
422
423
        if (array_key_exists($name, $this->_unsaved)) {
424
            unset($this->_unsaved[$name]);
425
        }
426
    }
427
428
    /////////////////////////////
429
    // ArrayAccess Interface
430
    /////////////////////////////
431
432
    public function offsetExists($offset)
433
    {
434
        return isset($this->$offset);
435
    }
436
437
    public function offsetGet($offset)
438
    {
439
        return $this->$offset;
440
    }
441
442
    public function offsetSet($offset, $value)
443
    {
444
        $this->$offset = $value;
445
    }
446
447
    public function offsetUnset($offset)
448
    {
449
        unset($this->$offset);
450
    }
451
452
    public static function __callStatic($name, $parameters)
453
    {
454
        // Any calls to unkown static methods should be deferred to
455
        // the query. This allows calls like User::where()
456
        // 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...
457
        return call_user_func_array([static::query(), $name], $parameters);
458
    }
459
460
    /////////////////////////////
461
    // Property Definitions
462
    /////////////////////////////
463
464
    /**
465
     * Gets all the property definitions for the model.
466
     *
467
     * @return array key-value map of properties
468
     */
469
    public static function getProperties()
470
    {
471
        return static::$properties;
472
    }
473
474
    /**
475
     * Gets a property defition for the model.
476
     *
477
     * @param string $property property to lookup
478
     *
479
     * @return array|null property
480
     */
481
    public static function getProperty($property)
482
    {
483
        return array_value(static::$properties, $property);
484
    }
485
486
    /**
487
     * Gets the names of the model ID properties.
488
     *
489
     * @return array
490
     */
491
    public static function getIDProperties()
492
    {
493
        return static::$ids;
494
    }
495
496
    /**
497
     * Checks if the model has a property.
498
     *
499
     * @param string $property property
500
     *
501
     * @return bool has property
502
     */
503
    public static function hasProperty($property)
504
    {
505
        return isset(static::$properties[$property]);
506
    }
507
508
    /**
509
     * Gets the mutator method name for a given proeprty name.
510
     * Looks for methods in the form of `setPropertyValue`.
511
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
512
     *
513
     * @param string $property property
514
     *
515
     * @return string|false method name if it exists
516
     */
517 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...
518
    {
519
        $class = get_called_class();
520
521
        $k = $class.':'.$property;
522
        if (!array_key_exists($k, self::$mutators)) {
523
            $inflector = Inflector::get();
524
            $method = 'set'.$inflector->camelize($property).'Value';
525
526
            if (!method_exists($class, $method)) {
527
                $method = false;
528
            }
529
530
            self::$mutators[$k] = $method;
531
        }
532
533
        return self::$mutators[$k];
534
    }
535
536
    /**
537
     * Gets the accessor method name for a given proeprty name.
538
     * Looks for methods in the form of `getPropertyValue`.
539
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
540
     *
541
     * @param string $property property
542
     *
543
     * @return string|false method name if it exists
544
     */
545 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...
546
    {
547
        $class = get_called_class();
548
549
        $k = $class.':'.$property;
550
        if (!array_key_exists($k, self::$accessors)) {
551
            $inflector = Inflector::get();
552
            $method = 'get'.$inflector->camelize($property).'Value';
553
554
            if (!method_exists($class, $method)) {
555
                $method = false;
556
            }
557
558
            self::$accessors[$k] = $method;
559
        }
560
561
        return self::$accessors[$k];
562
    }
563
564
    /**
565
     * Checks if a given property is a relationship.
566
     *
567
     * @param string $property
568
     *
569
     * @return bool
570
     */
571
    public static function isRelationship($property)
572
    {
573
        return in_array($property, static::$relationships);
574
    }
575
576
    /////////////////////////////
577
    // CRUD Operations
578
    /////////////////////////////
579
580
    /**
581
     * Saves the model.
582
     *
583
     * @return bool
584
     */
585
    public function save()
586
    {
587
        if ($this->_id === false) {
588
            return $this->create();
589
        }
590
591
        return $this->set($this->_unsaved);
592
    }
593
594
    /**
595
     * Creates a new model.
596
     *
597
     * @param array $data optional key-value properties to set
598
     *
599
     * @return bool
600
     *
601
     * @throws BadMethodCallException when called on an existing model
602
     */
603
    public function create(array $data = [])
604
    {
605
        if ($this->_id !== false) {
606
            throw new BadMethodCallException('Cannot call create() on an existing model');
607
        }
608
609
        if (!empty($data)) {
610
            foreach ($data as $k => $value) {
611
                $this->$k = $value;
612
            }
613
        }
614
615
        // dispatch the model.creating event
616
        $event = $this->dispatch(ModelEvent::CREATING);
617
        if ($event->isPropagationStopped()) {
618
            return false;
619
        }
620
621
        foreach (static::$properties as $name => $property) {
622
            // add in default values
623
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
624
                $this->_unsaved[$name] = $property['default'];
625
            }
626
        }
627
628
        // validate the values being saved
629
        $validated = true;
630
        $insertArray = [];
631
        foreach ($this->_unsaved as $name => $value) {
632
            // exclude if value does not map to a property
633
            if (!isset(static::$properties[$name])) {
634
                continue;
635
            }
636
637
            $property = static::$properties[$name];
638
639
            // cannot insert immutable values
640
            // (unless using the default value)
641
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
642
                continue;
643
            }
644
645
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
646
            $insertArray[$name] = $value;
647
        }
648
649
        // the final validation check is to look for required fields
650
        // it should be ran before returning (even if the validation
651
        // has already failed) in order to build a complete list of
652
        // validation errors
653
        if (!$this->hasRequiredValues($insertArray) || !$validated) {
654
            return false;
655
        }
656
657
        $created = self::$driver->createModel($this, $insertArray);
658
659 View Code Duplication
        if ($created) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
660
            // determine the model's new ID
661
            $this->_id = $this->getNewID();
662
663
            // NOTE clear the local cache before the model.created
664
            // event so that fetching values forces a reload
665
            // from the storage layer
666
            $this->clearCache();
667
668
            // dispatch the model.created event
669
            $event = $this->dispatch(ModelEvent::CREATED);
670
            if ($event->isPropagationStopped()) {
671
                return false;
672
            }
673
        }
674
675
        return $created;
676
    }
677
678
    /**
679
     * Ignores unsaved values when fetching the next value.
680
     *
681
     * @return self
682
     */
683
    public function ignoreUnsaved()
684
    {
685
        $this->_ignoreUnsaved = true;
686
687
        return $this;
688
    }
689
690
    /**
691
     * Fetches property values from the model.
692
     *
693
     * This method looks up values in this order:
694
     * IDs, local cache, unsaved values, storage layer, defaults
695
     *
696
     * @param array $properties list of property names to fetch values of
697
     *
698
     * @return array
699
     */
700
    public function get(array $properties)
701
    {
702
        // load the values from the IDs and local model cache
703
        $values = array_replace($this->ids(), $this->_values);
704
705
        // unless specified, use any unsaved values
706
        $ignoreUnsaved = $this->_ignoreUnsaved;
707
        $this->_ignoreUnsaved = false;
708
709
        if (!$ignoreUnsaved) {
710
            $values = array_replace($values, $this->_unsaved);
711
        }
712
713
        // attempt to load any missing values from the storage layer
714
        $missing = array_diff($properties, array_keys($values));
715
        if (count($missing) > 0) {
716
            // load values for the model
717
            $this->refresh();
718
            $values = array_replace($values, $this->_values);
719
720
            // add back any unsaved values
721
            if (!$ignoreUnsaved) {
722
                $values = array_replace($values, $this->_unsaved);
723
            }
724
        }
725
726
        return $this->buildGetResponse($properties, $values);
727
    }
728
729
    /**
730
     * Builds a key-value map of the requested properties given a set of values.
731
     *
732
     * @param array $properties
733
     * @param array $values
734
     *
735
     * @return array
736
     *
737
     * @throws InvalidArgumentException when a property was requested not present in the values
738
     */
739
    private function buildGetResponse(array $properties, array $values)
740
    {
741
        $response = [];
742
        foreach ($properties as $k) {
743
            $accessor = self::getAccessor($k);
744
745
            // use the supplied value if it's available
746
            if (array_key_exists($k, $values)) {
747
                $response[$k] = $values[$k];
748
            // set any missing values to the default value
749
            } elseif (static::hasProperty($k)) {
750
                $response[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
751
            // throw an exception for non-properties that do not
752
            // have an accessor
753
            } elseif (!$accessor) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $accessor of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
754
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
755
            // otherwise the value is considered null
756
            } else {
757
                $response[$k] = null;
758
            }
759
760
            // call any accessors
761
            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...
762
                $response[$k] = $this->$accessor($response[$k]);
763
            }
764
        }
765
766
        return $response;
767
    }
768
769
    /**
770
     * Gets the ID for a newly created model.
771
     *
772
     * @return string
773
     */
774
    protected function getNewID()
775
    {
776
        $ids = [];
777
        foreach (static::$ids as $k) {
778
            // attempt use the supplied value if the ID property is mutable
779
            $property = static::getProperty($k);
780
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
781
                $ids[] = $this->_unsaved[$k];
782
            } else {
783
                $ids[] = self::$driver->getCreatedID($this, $k);
784
            }
785
        }
786
787
        // TODO need to store the id as an array
788
        // instead of a string to maintain type integrity
789
        return (count($ids) > 1) ? implode(',', $ids) : $ids[0];
790
    }
791
792
    /**
793
     * Converts the model to an array.
794
     *
795
     * @return array model array
796
     */
797
    public function toArray()
798
    {
799
        // build the list of properties to retrieve
800
        $properties = array_keys(static::$properties);
801
802
        // remove any hidden properties
803
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
804
        $properties = array_diff($properties, $hide);
805
806
        // add any appended properties
807
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
808
        $properties = array_merge($properties, $append);
809
810
        // get the values for the properties
811
        $result = $this->get($properties);
812
813
        return $result;
814
    }
815
816
    /**
817
     * Updates the model.
818
     *
819
     * @param array $data optional key-value properties to set
820
     *
821
     * @return bool
822
     *
823
     * @throws BadMethodCallException when not called on an existing model
824
     */
825
    public function set(array $data = [])
826
    {
827
        if ($this->_id === false) {
828
            throw new BadMethodCallException('Can only call set() on an existing model');
829
        }
830
831
        // not updating anything?
832
        if (count($data) == 0) {
833
            return true;
834
        }
835
836
        // apply mutators
837
        foreach ($data as $k => $value) {
838
            if ($mutator = self::getMutator($k)) {
839
                $data[$k] = $this->$mutator($value);
840
            }
841
        }
842
843
        // dispatch the model.updating event
844
        $event = $this->dispatch(ModelEvent::UPDATING);
845
        if ($event->isPropagationStopped()) {
846
            return false;
847
        }
848
849
        // validate the values being saved
850
        $validated = true;
851
        $updateArray = [];
852
        foreach ($data as $name => $value) {
853
            // exclude if value does not map to a property
854
            if (!isset(static::$properties[$name])) {
855
                continue;
856
            }
857
858
            $property = static::$properties[$name];
859
860
            // can only modify mutable properties
861
            if ($property['mutable'] != self::MUTABLE) {
862
                continue;
863
            }
864
865
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
866
            $updateArray[$name] = $value;
867
        }
868
869
        if (!$validated) {
870
            return false;
871
        }
872
873
        $updated = self::$driver->updateModel($this, $updateArray);
874
875 View Code Duplication
        if ($updated) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
876
            // NOTE clear the local cache before the model.updated
877
            // event so that fetching values forces a reload
878
            // from the storage layer
879
            $this->clearCache();
880
881
            // dispatch the model.updated event
882
            $event = $this->dispatch(ModelEvent::UPDATED);
883
            if ($event->isPropagationStopped()) {
884
                return false;
885
            }
886
        }
887
888
        return $updated;
889
    }
890
891
    /**
892
     * Delete the model.
893
     *
894
     * @return bool success
895
     */
896
    public function delete()
897
    {
898
        if ($this->_id === false) {
899
            throw new BadMethodCallException('Can only call delete() on an existing model');
900
        }
901
902
        // dispatch the model.deleting event
903
        $event = $this->dispatch(ModelEvent::DELETING);
904
        if ($event->isPropagationStopped()) {
905
            return false;
906
        }
907
908
        $deleted = self::$driver->deleteModel($this);
909
910 View Code Duplication
        if ($deleted) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
911
            // dispatch the model.deleted event
912
            $event = $this->dispatch(ModelEvent::DELETED);
913
            if ($event->isPropagationStopped()) {
914
                return false;
915
            }
916
917
            // NOTE clear the local cache before the model.deleted
918
            // event so that fetching values forces a reload
919
            // from the storage layer
920
            $this->clearCache();
921
        }
922
923
        return $deleted;
924
    }
925
926
    /////////////////////////////
927
    // Queries
928
    /////////////////////////////
929
930
    /**
931
     * Generates a new query instance.
932
     *
933
     * @return Query
934
     */
935
    public static function query()
936
    {
937
        // Create a new model instance for the query to ensure
938
        // that the model's initialize() method gets called.
939
        // Otherwise, the property definitions will be incomplete.
940
        $model = new static();
941
942
        return new Query($model);
943
    }
944
945
    /**
946
     * Gets the toal number of records matching an optional criteria.
947
     *
948
     * @param array $where criteria
949
     *
950
     * @return int total
951
     */
952
    public static function totalRecords(array $where = [])
953
    {
954
        $query = static::query();
955
        $query->where($where);
956
957
        return self::getDriver()->totalRecords($query);
958
    }
959
960
    /**
961
     * Checks if the model exists in the database.
962
     *
963
     * @return bool
964
     */
965
    public function exists()
966
    {
967
        return static::totalRecords($this->ids()) == 1;
968
    }
969
970
    /**
971
     * @deprecated alias for refresh()
972
     */
973
    public function load()
974
    {
975
        return $this->refresh();
976
    }
977
978
    /**
979
     * Loads the model from the storage layer.
980
     *
981
     * @return self
982
     */
983
    public function refresh()
984
    {
985
        if ($this->_id === false) {
986
            return $this;
987
        }
988
989
        $values = self::$driver->loadModel($this);
990
991
        if (!is_array($values)) {
992
            return $this;
993
        }
994
995
        return $this->refreshWith($values);
996
    }
997
998
    /**
999
     * Loads values into the model.
1000
     *
1001
     * @param array $values values
1002
     *
1003
     * @return self
1004
     */
1005
    public function refreshWith(array $values)
1006
    {
1007
        $this->_values = $values;
1008
1009
        return $this;
1010
    }
1011
1012
    /**
1013
     * Clears the cache for this model.
1014
     *
1015
     * @return self
1016
     */
1017
    public function clearCache()
1018
    {
1019
        $this->_unsaved = [];
1020
        $this->_values = [];
1021
1022
        return $this;
1023
    }
1024
1025
    /////////////////////////////
1026
    // Relationships
1027
    /////////////////////////////
1028
1029
    /**
1030
     * Creates the parent side of a One-To-One relationship.
1031
     *
1032
     * @param string $model      foreign model class
1033
     * @param string $foreignKey identifying key on foreign model
1034
     * @param string $localKey   identifying key on local model
1035
     *
1036
     * @return \Pulsar\Relation\Relation
1037
     */
1038 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...
1039
    {
1040
        // the default local key would look like `user_id`
1041
        // for a model named User
1042
        if (!$foreignKey) {
1043
            $inflector = Inflector::get();
1044
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1045
        }
1046
1047
        if (!$localKey) {
1048
            $localKey = self::DEFAULT_ID_PROPERTY;
1049
        }
1050
1051
        return new HasOne($model, $foreignKey, $localKey, $this);
1052
    }
1053
1054
    /**
1055
     * Creates the child side of a One-To-One or One-To-Many relationship.
1056
     *
1057
     * @param string $model      foreign model class
1058
     * @param string $foreignKey identifying key on foreign model
1059
     * @param string $localKey   identifying key on local model
1060
     *
1061
     * @return \Pulsar\Relation\Relation
1062
     */
1063 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...
1064
    {
1065
        if (!$foreignKey) {
1066
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1067
        }
1068
1069
        // the default local key would look like `user_id`
1070
        // for a model named User
1071
        if (!$localKey) {
1072
            $inflector = Inflector::get();
1073
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1074
        }
1075
1076
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1077
    }
1078
1079
    /**
1080
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1081
     *
1082
     * @param string $model      foreign model class
1083
     * @param string $foreignKey identifying key on foreign model
1084
     * @param string $localKey   identifying key on local model
1085
     *
1086
     * @return \Pulsar\Relation\Relation
1087
     */
1088 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...
1089
    {
1090
        // the default local key would look like `user_id`
1091
        // for a model named User
1092
        if (!$foreignKey) {
1093
            $inflector = Inflector::get();
1094
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1095
        }
1096
1097
        if (!$localKey) {
1098
            $localKey = self::DEFAULT_ID_PROPERTY;
1099
        }
1100
1101
        return new HasMany($model, $foreignKey, $localKey, $this);
1102
    }
1103
1104
    /**
1105
     * Creates the child side of a Many-To-Many relationship.
1106
     *
1107
     * @param string $model      foreign model class
1108
     * @param string $foreignKey identifying key on foreign model
1109
     * @param string $localKey   identifying key on local model
1110
     *
1111
     * @return \Pulsar\Relation\Relation
1112
     */
1113 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...
1114
    {
1115
        if (!$foreignKey) {
1116
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1117
        }
1118
1119
        // the default local key would look like `user_id`
1120
        // for a model named User
1121
        if (!$localKey) {
1122
            $inflector = Inflector::get();
1123
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1124
        }
1125
1126
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1127
    }
1128
1129
    /**
1130
     * Loads a given relationship (if not already) and returns
1131
     * its results.
1132
     *
1133
     * @param string $name
1134
     *
1135
     * @return mixed
1136
     */
1137
    protected function loadRelationship($name)
1138
    {
1139
        if (!isset($this->_values[$name])) {
1140
            $relationship = $this->$name();
1141
            $this->_values[$name] = $relationship->getResults();
1142
        }
1143
1144
        return $this->_values[$name];
1145
    }
1146
1147
    /////////////////////////////
1148
    // Events
1149
    /////////////////////////////
1150
1151
    /**
1152
     * Gets the event dispatcher.
1153
     *
1154
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1155
     */
1156
    public static function getDispatcher($ignoreCache = false)
1157
    {
1158
        $class = get_called_class();
1159
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1160
            self::$dispatchers[$class] = new EventDispatcher();
1161
        }
1162
1163
        return self::$dispatchers[$class];
1164
    }
1165
1166
    /**
1167
     * Subscribes to a listener to an event.
1168
     *
1169
     * @param string   $event    event name
1170
     * @param callable $listener
1171
     * @param int      $priority optional priority, higher #s get called first
1172
     */
1173
    public static function listen($event, callable $listener, $priority = 0)
1174
    {
1175
        static::getDispatcher()->addListener($event, $listener, $priority);
1176
    }
1177
1178
    /**
1179
     * Adds a listener to the model.creating event.
1180
     *
1181
     * @param callable $listener
1182
     * @param int      $priority
1183
     */
1184
    public static function creating(callable $listener, $priority = 0)
1185
    {
1186
        static::listen(ModelEvent::CREATING, $listener, $priority);
1187
    }
1188
1189
    /**
1190
     * Adds a listener to the model.created event.
1191
     *
1192
     * @param callable $listener
1193
     * @param int      $priority
1194
     */
1195
    public static function created(callable $listener, $priority = 0)
1196
    {
1197
        static::listen(ModelEvent::CREATED, $listener, $priority);
1198
    }
1199
1200
    /**
1201
     * Adds a listener to the model.updating event.
1202
     *
1203
     * @param callable $listener
1204
     * @param int      $priority
1205
     */
1206
    public static function updating(callable $listener, $priority = 0)
1207
    {
1208
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1209
    }
1210
1211
    /**
1212
     * Adds a listener to the model.updated event.
1213
     *
1214
     * @param callable $listener
1215
     * @param int      $priority
1216
     */
1217
    public static function updated(callable $listener, $priority = 0)
1218
    {
1219
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1220
    }
1221
1222
    /**
1223
     * Adds a listener to the model.deleting event.
1224
     *
1225
     * @param callable $listener
1226
     * @param int      $priority
1227
     */
1228
    public static function deleting(callable $listener, $priority = 0)
1229
    {
1230
        static::listen(ModelEvent::DELETING, $listener, $priority);
1231
    }
1232
1233
    /**
1234
     * Adds a listener to the model.deleted event.
1235
     *
1236
     * @param callable $listener
1237
     * @param int      $priority
1238
     */
1239
    public static function deleted(callable $listener, $priority = 0)
1240
    {
1241
        static::listen(ModelEvent::DELETED, $listener, $priority);
1242
    }
1243
1244
    /**
1245
     * Dispatches an event.
1246
     *
1247
     * @param string $eventName
1248
     *
1249
     * @return ModelEvent
1250
     */
1251
    protected function dispatch($eventName)
1252
    {
1253
        $event = new ModelEvent($this);
1254
1255
        return static::getDispatcher()->dispatch($eventName, $event);
1256
    }
1257
1258
    /////////////////////////////
1259
    // Validation
1260
    /////////////////////////////
1261
1262
    /**
1263
     * Gets the error stack for this model instance. Used to
1264
     * keep track of validation errors.
1265
     *
1266
     * @return \Infuse\ErrorStack
1267
     */
1268
    public function getErrors()
1269
    {
1270
        if (!$this->_errors) {
1271
            $this->_errors = new ErrorStack($this->app);
1272
        }
1273
1274
        return $this->_errors;
1275
    }
1276
1277
    /**
1278
     * Validates and marshals a value to storage.
1279
     *
1280
     * @param array  $property
1281
     * @param string $propertyName
1282
     * @param mixed  $value
1283
     *
1284
     * @return bool
1285
     */
1286
    private function filterAndValidate(array $property, $propertyName, &$value)
1287
    {
1288
        // assume empty string is a null value for properties
1289
        // that are marked as optionally-null
1290
        if ($property['null'] && empty($value)) {
1291
            $value = null;
1292
1293
            return true;
1294
        }
1295
1296
        // validate
1297
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1298
1299
        // unique?
1300
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1301
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1302
        }
1303
1304
        return $valid;
1305
    }
1306
1307
    /**
1308
     * Validates a value for a property.
1309
     *
1310
     * @param array  $property
1311
     * @param string $propertyName
1312
     * @param mixed  $value
1313
     *
1314
     * @return bool
1315
     */
1316
    private function validate(array $property, $propertyName, $value)
1317
    {
1318
        $valid = true;
1319
1320
        if (isset($property['validate']) && is_callable($property['validate'])) {
1321
            $valid = call_user_func_array($property['validate'], [$value]);
1322
        } elseif (isset($property['validate'])) {
1323
            $valid = Validate::is($value, $property['validate']);
1324
        }
1325
1326
        if (!$valid) {
1327
            $this->getErrors()->push([
1328
                'error' => self::ERROR_VALIDATION_FAILED,
1329
                'params' => [
1330
                    'field' => $propertyName,
1331
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1332
        }
1333
1334
        return [$valid, $value];
1335
    }
1336
1337
    /**
1338
     * Checks if a value is unique for a property.
1339
     *
1340
     * @param array  $property
1341
     * @param string $propertyName
1342
     * @param mixed  $value
1343
     *
1344
     * @return bool
1345
     */
1346
    private function checkUniqueness(array $property, $propertyName, $value)
1347
    {
1348
        if (static::totalRecords([$propertyName => $value]) > 0) {
1349
            $this->getErrors()->push([
1350
                'error' => self::ERROR_NOT_UNIQUE,
1351
                'params' => [
1352
                    'field' => $propertyName,
1353
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1354
1355
            return false;
1356
        }
1357
1358
        return true;
1359
    }
1360
1361
    /**
1362
     * Checks if an input has all of the required values. Adds
1363
     * messages for any missing values to the error stack.
1364
     *
1365
     * @param array $values
1366
     *
1367
     * @return bool
1368
     */
1369
    private function hasRequiredValues(array $values)
1370
    {
1371
        $hasRequired = true;
1372
        foreach (static::$properties as $name => $property) {
1373
            if ($property['required'] && !isset($values[$name])) {
1374
                $property = static::$properties[$name];
1375
                $this->getErrors()->push([
1376
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
1377
                    'params' => [
1378
                        'field' => $name,
1379
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
1380
1381
                $hasRequired = false;
1382
            }
1383
        }
1384
1385
        return $hasRequired;
1386
    }
1387
1388
    /**
1389
     * Gets the marshaled default value for a property (if set).
1390
     *
1391
     * @param string $property
1392
     *
1393
     * @return mixed
1394
     */
1395
    private function getPropertyDefault(array $property)
1396
    {
1397
        return array_value($property, 'default');
1398
    }
1399
}
1400