Completed
Push — master ( 63bdcf...dddc7a )
by Jared
02:31
created

Model::belongsToMany()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 15
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 15
loc 15
rs 9.4286
cc 3
eloc 7
nc 4
nop 3
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 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 \Pimple\Container
66
     */
67
    protected static $injectedApp;
68
69
    /**
70
     * @staticvar array
71
     */
72
    protected static $dispatchers;
73
74
    /**
75
     * @var number|string|bool
76
     */
77
    protected $_id;
78
79
    /**
80
     * @var \Pimple\Container
81
     */
82
    protected $app;
83
84
    /**
85
     * @var array
86
     */
87
    protected $_values = [];
88
89
    /**
90
     * @var array
91
     */
92
    protected $_unsaved = [];
93
94
    /**
95
     * @var array
96
     */
97
    protected $_relationships = [];
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 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
        $result = $this->get([$name]);
365
366
        return reset($result);
367
    }
368
369
    /**
370
     * Sets an unsaved value.
371
     *
372
     * @param string $name
373
     * @param mixed  $value
374
     */
375
    public function __set($name, $value)
376
    {
377
        // if changing property, remove relation model
378
        if (isset($this->_relationships[$name])) {
379
            unset($this->_relationships[$name]);
380
        }
381
382
        // call any mutators
383
        $mutator = self::getMutator($name);
384
        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...
385
            $this->_unsaved[$name] = $this->$mutator($value);
386
        } else {
387
            $this->_unsaved[$name] = $value;
388
        }
389
    }
390
391
    /**
392
     * Checks if an unsaved value or property exists by this name.
393
     *
394
     * @param string $name
395
     *
396
     * @return bool
397
     */
398
    public function __isset($name)
399
    {
400
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
401
    }
402
403
    /**
404
     * Unsets an unsaved value.
405
     *
406
     * @param string $name
407
     */
408
    public function __unset($name)
409
    {
410
        if (array_key_exists($name, $this->_unsaved)) {
411
            // if changing property, remove relation model
412
            if (isset($this->_relationships[$name])) {
413
                unset($this->_relationships[$name]);
414
            }
415
416
            unset($this->_unsaved[$name]);
417
        }
418
    }
419
420
    /////////////////////////////
421
    // ArrayAccess Interface
422
    /////////////////////////////
423
424
    public function offsetExists($offset)
425
    {
426
        return isset($this->$offset);
427
    }
428
429
    public function offsetGet($offset)
430
    {
431
        return $this->$offset;
432
    }
433
434
    public function offsetSet($offset, $value)
435
    {
436
        $this->$offset = $value;
437
    }
438
439
    public function offsetUnset($offset)
440
    {
441
        unset($this->$offset);
442
    }
443
444
    public static function __callStatic($name, $parameters)
445
    {
446
        // Any calls to unkown static methods should be deferred to
447
        // the query. This allows calls like User::where()
448
        // 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...
449
        return call_user_func_array([static::query(), $name], $parameters);
450
    }
451
452
    /////////////////////////////
453
    // Property Definitions
454
    /////////////////////////////
455
456
    /**
457
     * Gets all the property definitions for the model.
458
     *
459
     * @return array key-value map of properties
460
     */
461
    public static function getProperties()
462
    {
463
        return static::$properties;
464
    }
465
466
    /**
467
     * Gets a property defition for the model.
468
     *
469
     * @param string $property property to lookup
470
     *
471
     * @return array|null property
472
     */
473
    public static function getProperty($property)
474
    {
475
        return array_value(static::$properties, $property);
476
    }
477
478
    /**
479
     * Gets the names of the model ID properties.
480
     *
481
     * @return array
482
     */
483
    public static function getIDProperties()
484
    {
485
        return static::$ids;
486
    }
487
488
    /**
489
     * Checks if the model has a property.
490
     *
491
     * @param string $property property
492
     *
493
     * @return bool has property
494
     */
495
    public static function hasProperty($property)
496
    {
497
        return isset(static::$properties[$property]);
498
    }
499
500
    /**
501
     * Gets the mutator method name for a given proeprty name.
502
     * Looks for methods in the form of `setPropertyValue`.
503
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
504
     *
505
     * @param string $property property
506
     *
507
     * @return string|false method name if it exists
508
     */
509 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...
510
    {
511
        $class = get_called_class();
512
513
        $k = $class.':'.$property;
514
        if (!array_key_exists($k, self::$mutators)) {
515
            $inflector = Inflector::get();
516
            $method = 'set'.$inflector->camelize($property).'Value';
517
518
            if (!method_exists($class, $method)) {
519
                $method = false;
520
            }
521
522
            self::$mutators[$k] = $method;
523
        }
524
525
        return self::$mutators[$k];
526
    }
527
528
    /**
529
     * Gets the accessor method name for a given proeprty name.
530
     * Looks for methods in the form of `getPropertyValue`.
531
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
532
     *
533
     * @param string $property property
534
     *
535
     * @return string|false method name if it exists
536
     */
537 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...
538
    {
539
        $class = get_called_class();
540
541
        $k = $class.':'.$property;
542
        if (!array_key_exists($k, self::$accessors)) {
543
            $inflector = Inflector::get();
544
            $method = 'get'.$inflector->camelize($property).'Value';
545
546
            if (!method_exists($class, $method)) {
547
                $method = false;
548
            }
549
550
            self::$accessors[$k] = $method;
551
        }
552
553
        return self::$accessors[$k];
554
    }
555
556
    /////////////////////////////
557
    // CRUD Operations
558
    /////////////////////////////
559
560
    /**
561
     * Saves the model.
562
     *
563
     * @return bool
564
     */
565
    public function save()
566
    {
567
        if ($this->_id === false) {
568
            return $this->create();
569
        }
570
571
        return $this->set($this->_unsaved);
572
    }
573
574
    /**
575
     * Creates a new model.
576
     *
577
     * @param array $data optional key-value properties to set
578
     *
579
     * @return bool
580
     *
581
     * @throws BadMethodCallException when called on an existing model
582
     */
583
    public function create(array $data = [])
584
    {
585
        if ($this->_id !== false) {
586
            throw new BadMethodCallException('Cannot call create() on an existing model');
587
        }
588
589
        if (!empty($data)) {
590
            foreach ($data as $k => $value) {
591
                $this->$k = $value;
592
            }
593
        }
594
595
        // dispatch the model.creating event
596
        if (!$this->handleDispatch(ModelEvent::CREATING)) {
597
            return false;
598
        }
599
600
        $requiredProperties = [];
601
        foreach (static::$properties as $name => $property) {
602
            // build a list of the required properties
603
            if ($property['required']) {
604
                $requiredProperties[] = $name;
605
            }
606
607
            // add in default values
608
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
609
                $this->_unsaved[$name] = $property['default'];
610
            }
611
        }
612
613
        // validate the values being saved
614
        $validated = true;
615
        $insertArray = [];
616
        foreach ($this->_unsaved as $name => $value) {
617
            // exclude if value does not map to a property
618
            if (!isset(static::$properties[$name])) {
619
                continue;
620
            }
621
622
            $property = static::$properties[$name];
623
624
            // cannot insert immutable values
625
            // (unless using the default value)
626
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
627
                continue;
628
            }
629
630
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
631
            $insertArray[$name] = $value;
632
        }
633
634
        // check for required fields
635
        foreach ($requiredProperties as $name) {
636
            if (!isset($insertArray[$name])) {
637
                $property = static::$properties[$name];
638
                $this->getErrors()->push([
639
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
640
                    'params' => [
641
                        'field' => $name,
642
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
643
644
                $validated = false;
645
            }
646
        }
647
648
        if (!$validated) {
649
            return false;
650
        }
651
652
        $created = self::$driver->createModel($this, $insertArray);
653
654
        if ($created) {
655
            // determine the model's new ID
656
            $this->_id = $this->getNewID();
657
658
            // NOTE clear the local cache before the model.created
659
            // event so that fetching values forces a reload
660
            // from the storage layer
661
            $this->clearCache();
662
663
            // dispatch the model.created event
664
            if (!$this->handleDispatch(ModelEvent::CREATED)) {
665
                return false;
666
            }
667
        }
668
669
        return $created;
670
    }
671
672
    /**
673
     * Ignores unsaved values when fetching the next value.
674
     *
675
     * @return self
676
     */
677
    public function ignoreUnsaved()
678
    {
679
        $this->_ignoreUnsaved = true;
680
681
        return $this;
682
    }
683
684
    /**
685
     * Fetches property values from the model.
686
     *
687
     * This method looks up values in this order:
688
     * IDs, local cache, unsaved values, storage layer, defaults
689
     *
690
     * @param array $properties list of property names to fetch values of
691
     *
692
     * @return array
693
     */
694
    public function get(array $properties)
695
    {
696
        // load the values from the IDs and local model cache
697
        $values = array_replace($this->ids(), $this->_values);
698
699
        // unless specified, use any unsaved values
700
        $ignoreUnsaved = $this->_ignoreUnsaved;
701
        $this->_ignoreUnsaved = false;
702
703
        if (!$ignoreUnsaved) {
704
            $values = array_replace($values, $this->_unsaved);
705
        }
706
707
        // attempt to load any missing values from the storage layer
708
        $missing = array_diff($properties, array_keys($values));
709
        if (count($missing) > 0) {
710
            // load values for the model
711
            $this->refresh();
712
            $values = array_replace($values, $this->_values);
713
714
            // add back any unsaved values
715
            if (!$ignoreUnsaved) {
716
                $values = array_replace($values, $this->_unsaved);
717
            }
718
        }
719
720
        return $this->buildGetResponse($properties, $values);
721
    }
722
723
    /**
724
     * Builds a key-value map of the requested properties given a set of values
725
     *
726
     * @param array $properties
727
     * @param array $values
728
     *
729
     * @return array
730
     *
731
     * @throws InvalidArgumentException when a property was requested not present in the values
732
     */
733
    private function buildGetResponse(array $properties, array $values)
734
    {
735
        $response = [];
736
        foreach ($properties as $k) {
737
            $accessor = self::getAccessor($k);
738
739
            // use the supplied value if it's available
740
            if (array_key_exists($k, $values)) {
741
                $response[$k] = $values[$k];
742
            // set any missing values to the default value
743
            } elseif (static::hasProperty($k)) {
744
                $response[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
745
            // throw an exception for non-properties that do not
746
            // have an accessor
747
            } 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...
748
                throw new InvalidArgumentException(static::modelName().' does not have a `'.$k.'` property.');
749
            // otherwise the value is considered null
750
            } else {
751
                $response[$k] = null;
752
            }
753
754
            // call any accessors
755
            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...
756
                $response[$k] = $this->$accessor($response[$k]);
757
            }
758
        }
759
760
        return $response;
761
    }
762
763
    /**
764
     * Gets the ID for a newly created model.
765
     *
766
     * @return string
767
     */
768
    protected function getNewID()
769
    {
770
        $ids = [];
771
        foreach (static::$ids as $k) {
772
            // attempt use the supplied value if the ID property is mutable
773
            $property = static::getProperty($k);
774
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
775
                $ids[] = $this->_unsaved[$k];
776
            } else {
777
                $ids[] = self::$driver->getCreatedID($this, $k);
778
            }
779
        }
780
781
        // TODO need to store the id as an array
782
        // instead of a string to maintain type integrity
783
        return (count($ids) > 1) ? implode(',', $ids) : $ids[0];
784
    }
785
786
    /**
787
     * Converts the model to an array.
788
     *
789
     * @return array model array
790
     */
791
    public function toArray()
792
    {
793
        // build the list of properties to retrieve
794
        $properties = array_keys(static::$properties);
795
796
        // remove any hidden properties
797
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
798
        $properties = array_diff($properties, $hide);
799
800
        // add any appended properties
801
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
802
        $properties = array_merge($properties, $append);
803
804
        // get the values for the properties
805
        $result = $this->get($properties);
806
807
        return $result;
808
    }
809
810
    /**
811
     * Updates the model.
812
     *
813
     * @param array $data optional key-value properties to set
814
     *
815
     * @return bool
816
     *
817
     * @throws BadMethodCallException when not called on an existing model
818
     */
819
    public function set(array $data = [])
820
    {
821
        if ($this->_id === false) {
822
            throw new BadMethodCallException('Can only call set() on an existing model');
823
        }
824
825
        // not updating anything?
826
        if (count($data) == 0) {
827
            return true;
828
        }
829
830
        // apply mutators
831
        foreach ($data as $k => $value) {
832
            if ($mutator = self::getMutator($k)) {
833
                $data[$k] = $this->$mutator($value);
834
            }
835
        }
836
837
        // dispatch the model.updating event
838
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
839
            return false;
840
        }
841
842
        // validate the values being saved
843
        $validated = true;
844
        $updateArray = [];
845
        foreach ($data as $name => $value) {
846
            // exclude if value does not map to a property
847
            if (!isset(static::$properties[$name])) {
848
                continue;
849
            }
850
851
            $property = static::$properties[$name];
852
853
            // can only modify mutable properties
854
            if ($property['mutable'] != self::MUTABLE) {
855
                continue;
856
            }
857
858
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
859
            $updateArray[$name] = $value;
860
        }
861
862
        if (!$validated) {
863
            return false;
864
        }
865
866
        $updated = self::$driver->updateModel($this, $updateArray);
867
868
        if ($updated) {
869
            // NOTE clear the local cache before the model.updated
870
            // event so that fetching values forces a reload
871
            // from the storage layer
872
            $this->clearCache();
873
874
            // dispatch the model.updated event
875
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
876
                return false;
877
            }
878
        }
879
880
        return $updated;
881
    }
882
883
    /**
884
     * Delete the model.
885
     *
886
     * @return bool success
887
     */
888
    public function delete()
889
    {
890
        if ($this->_id === false) {
891
            throw new BadMethodCallException('Can only call delete() on an existing model');
892
        }
893
894
        // dispatch the model.deleting event
895
        if (!$this->handleDispatch(ModelEvent::DELETING)) {
896
            return false;
897
        }
898
899
        $deleted = self::$driver->deleteModel($this);
900
901
        if ($deleted) {
902
            // dispatch the model.deleted event
903
            if (!$this->handleDispatch(ModelEvent::DELETED)) {
904
                return false;
905
            }
906
907
            // NOTE clear the local cache before the model.deleted
908
            // event so that fetching values forces a reload
909
            // from the storage layer
910
            $this->clearCache();
911
        }
912
913
        return $deleted;
914
    }
915
916
    /////////////////////////////
917
    // Queries
918
    /////////////////////////////
919
920
    /**
921
     * Generates a new query instance.
922
     *
923
     * @return Model\Query
924
     */
925
    public static function query()
926
    {
927
        // Create a new model instance for the query to ensure
928
        // that the model's initialize() method gets called.
929
        // Otherwise, the property definitions will be incomplete.
930
        $model = new static();
931
932
        return new Query($model);
933
    }
934
935
    /**
936
     * Gets the toal number of records matching an optional criteria.
937
     *
938
     * @param array $where criteria
939
     *
940
     * @return int total
941
     */
942
    public static function totalRecords(array $where = [])
943
    {
944
        $query = static::query();
945
        $query->where($where);
946
947
        return self::getDriver()->totalRecords($query);
948
    }
949
950
    /**
951
     * Checks if the model exists in the database.
952
     *
953
     * @return bool
954
     */
955
    public function exists()
956
    {
957
        return static::totalRecords($this->ids()) == 1;
958
    }
959
960
    /**
961
     * @deprecated alias for refresh()
962
     */
963
    public function load()
964
    {
965
        return $this->refresh();
966
    }
967
968
    /**
969
     * Loads the model from the storage layer.
970
     *
971
     * @return self
972
     */
973
    public function refresh()
974
    {
975
        if ($this->_id === false) {
976
            return $this;
977
        }
978
979
        $values = self::$driver->loadModel($this);
980
981
        if (!is_array($values)) {
982
            return $this;
983
        }
984
985
        // clear any relations
986
        $this->_relationships = [];
987
988
        return $this->refreshWith($values);
989
    }
990
991
    /**
992
     * Loads values into the model.
993
     *
994
     * @param array $values values
995
     *
996
     * @return self
997
     */
998
    public function refreshWith(array $values)
999
    {
1000
        $this->_values = $values;
1001
1002
        return $this;
1003
    }
1004
1005
    /**
1006
     * Clears the cache for this model.
1007
     *
1008
     * @return self
1009
     */
1010
    public function clearCache()
1011
    {
1012
        $this->_unsaved = [];
1013
        $this->_values = [];
1014
        $this->_relationships = [];
1015
1016
        return $this;
1017
    }
1018
1019
    /////////////////////////////
1020
    // Relationships
1021
    /////////////////////////////
1022
1023
    /**
1024
     * Gets the model object corresponding to a relation
1025
     * WARNING no check is used to see if the model returned actually exists.
1026
     *
1027
     * @param string $propertyName property
1028
     *
1029
     * @return \Pulsar\Model model
1030
     */
1031
    public function relation($propertyName)
1032
    {
1033
        // TODO deprecated
1034
        $property = static::getProperty($propertyName);
1035
1036
        if (!isset($this->_relationships[$propertyName])) {
1037
            $relationModelName = $property['relation'];
1038
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1039
        }
1040
1041
        return $this->_relationships[$propertyName];
1042
    }
1043
1044
    /**
1045
     * Creates the parent side of a One-To-One relationship.
1046
     *
1047
     * @param string $model      foreign model class
1048
     * @param string $foreignKey identifying key on foreign model
1049
     * @param string $localKey   identifying key on local model
1050
     *
1051
     * @return Relation
1052
     */
1053 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...
1054
    {
1055
        // the default local key would look like `user_id`
1056
        // for a model named User
1057
        if (!$foreignKey) {
1058
            $inflector = Inflector::get();
1059
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1060
        }
1061
1062
        if (!$localKey) {
1063
            $localKey = self::DEFAULT_ID_PROPERTY;
1064
        }
1065
1066
        return new HasOne($model, $foreignKey, $localKey, $this);
1067
    }
1068
1069
    /**
1070
     * Creates the child side of a One-To-One or One-To-Many relationship.
1071
     *
1072
     * @param string $model      foreign model class
1073
     * @param string $foreignKey identifying key on foreign model
1074
     * @param string $localKey   identifying key on local model
1075
     *
1076
     * @return Relation
1077
     */
1078 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...
1079
    {
1080
        if (!$foreignKey) {
1081
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1082
        }
1083
1084
        // the default local key would look like `user_id`
1085
        // for a model named User
1086
        if (!$localKey) {
1087
            $inflector = Inflector::get();
1088
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1089
        }
1090
1091
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1092
    }
1093
1094
    /**
1095
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1096
     *
1097
     * @param string $model      foreign model class
1098
     * @param string $foreignKey identifying key on foreign model
1099
     * @param string $localKey   identifying key on local model
1100
     *
1101
     * @return Relation
1102
     */
1103 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...
1104
    {
1105
        // the default local key would look like `user_id`
1106
        // for a model named User
1107
        if (!$foreignKey) {
1108
            $inflector = Inflector::get();
1109
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1110
        }
1111
1112
        if (!$localKey) {
1113
            $localKey = self::DEFAULT_ID_PROPERTY;
1114
        }
1115
1116
        return new HasMany($model, $foreignKey, $localKey, $this);
1117
    }
1118
1119
    /**
1120
     * Creates the child side of a Many-To-Many relationship.
1121
     *
1122
     * @param string $model      foreign model class
1123
     * @param string $foreignKey identifying key on foreign model
1124
     * @param string $localKey   identifying key on local model
1125
     *
1126
     * @return Relation
1127
     */
1128 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...
1129
    {
1130
        if (!$foreignKey) {
1131
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1132
        }
1133
1134
        // the default local key would look like `user_id`
1135
        // for a model named User
1136
        if (!$localKey) {
1137
            $inflector = Inflector::get();
1138
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1139
        }
1140
1141
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1142
    }
1143
1144
    /////////////////////////////
1145
    // Events
1146
    /////////////////////////////
1147
1148
    /**
1149
     * Gets the event dispatcher.
1150
     *
1151
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
1152
     */
1153
    public static function getDispatcher($ignoreCache = false)
1154
    {
1155
        $class = get_called_class();
1156
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1157
            self::$dispatchers[$class] = new EventDispatcher();
1158
        }
1159
1160
        return self::$dispatchers[$class];
1161
    }
1162
1163
    /**
1164
     * Subscribes to a listener to an event.
1165
     *
1166
     * @param string   $event    event name
1167
     * @param callable $listener
1168
     * @param int      $priority optional priority, higher #s get called first
1169
     */
1170
    public static function listen($event, callable $listener, $priority = 0)
1171
    {
1172
        static::getDispatcher()->addListener($event, $listener, $priority);
1173
    }
1174
1175
    /**
1176
     * Adds a listener to the model.creating event.
1177
     *
1178
     * @param callable $listener
1179
     * @param int      $priority
1180
     */
1181
    public static function creating(callable $listener, $priority = 0)
1182
    {
1183
        static::listen(ModelEvent::CREATING, $listener, $priority);
1184
    }
1185
1186
    /**
1187
     * Adds a listener to the model.created event.
1188
     *
1189
     * @param callable $listener
1190
     * @param int      $priority
1191
     */
1192
    public static function created(callable $listener, $priority = 0)
1193
    {
1194
        static::listen(ModelEvent::CREATED, $listener, $priority);
1195
    }
1196
1197
    /**
1198
     * Adds a listener to the model.updating event.
1199
     *
1200
     * @param callable $listener
1201
     * @param int      $priority
1202
     */
1203
    public static function updating(callable $listener, $priority = 0)
1204
    {
1205
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1206
    }
1207
1208
    /**
1209
     * Adds a listener to the model.updated event.
1210
     *
1211
     * @param callable $listener
1212
     * @param int      $priority
1213
     */
1214
    public static function updated(callable $listener, $priority = 0)
1215
    {
1216
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1217
    }
1218
1219
    /**
1220
     * Adds a listener to the model.deleting event.
1221
     *
1222
     * @param callable $listener
1223
     * @param int      $priority
1224
     */
1225
    public static function deleting(callable $listener, $priority = 0)
1226
    {
1227
        static::listen(ModelEvent::DELETING, $listener, $priority);
1228
    }
1229
1230
    /**
1231
     * Adds a listener to the model.deleted event.
1232
     *
1233
     * @param callable $listener
1234
     * @param int      $priority
1235
     */
1236
    public static function deleted(callable $listener, $priority = 0)
1237
    {
1238
        static::listen(ModelEvent::DELETED, $listener, $priority);
1239
    }
1240
1241
    /**
1242
     * Dispatches an event.
1243
     *
1244
     * @param string $eventName
1245
     *
1246
     * @return Model\ModelEvent
1247
     */
1248
    protected function dispatch($eventName)
1249
    {
1250
        $event = new ModelEvent($this);
1251
1252
        return static::getDispatcher()->dispatch($eventName, $event);
1253
    }
1254
1255
    /**
1256
     * Dispatches the given event and checks if it was successful.
1257
     *
1258
     * @param string $eventName
1259
     *
1260
     * @return bool
1261
     */
1262
    private function handleDispatch($eventName)
1263
    {
1264
        $event = $this->dispatch($eventName);
1265
        
1266
        return !$event->isPropagationStopped();
1267
    }
1268
1269
    /////////////////////////////
1270
    // Validation
1271
    /////////////////////////////
1272
1273
    /**
1274
     * Gets the error stack for this model instance. Used to
1275
     * keep track of validation errors.
1276
     *
1277
     * @return \Infuse\ErrorStack
1278
     */
1279
    function getErrors()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1280
    {
1281
        if (!$this->_errors) {
1282
            $this->_errors = new ErrorStack($this->app);
1283
        }
1284
1285
        return $this->_errors;
1286
    }
1287
1288
    /**
1289
     * Validates and marshals a value to storage.
1290
     *
1291
     * @param array  $property
1292
     * @param string $propertyName
1293
     * @param mixed  $value
1294
     *
1295
     * @return bool
1296
     */
1297
    private function filterAndValidate(array $property, $propertyName, &$value)
1298
    {
1299
        // assume empty string is a null value for properties
1300
        // that are marked as optionally-null
1301
        if ($property['null'] && empty($value)) {
1302
            $value = null;
1303
1304
            return true;
1305
        }
1306
1307
        // validate
1308
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1309
1310
        // unique?
1311
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1312
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1313
        }
1314
1315
        return $valid;
1316
    }
1317
1318
    /**
1319
     * Validates a value for a property.
1320
     *
1321
     * @param array  $property
1322
     * @param string $propertyName
1323
     * @param mixed  $value
1324
     *
1325
     * @return bool
1326
     */
1327
    private function validate(array $property, $propertyName, $value)
1328
    {
1329
        $valid = true;
1330
1331
        if (isset($property['validate']) && is_callable($property['validate'])) {
1332
            $valid = call_user_func_array($property['validate'], [$value]);
1333
        } elseif (isset($property['validate'])) {
1334
            $valid = Validate::is($value, $property['validate']);
1335
        }
1336
1337
        if (!$valid) {
1338
            $this->getErrors()->push([
1339
                'error' => self::ERROR_VALIDATION_FAILED,
1340
                'params' => [
1341
                    'field' => $propertyName,
1342
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1343
        }
1344
1345
        return [$valid, $value];
1346
    }
1347
1348
    /**
1349
     * Checks if a value is unique for a property.
1350
     *
1351
     * @param array  $property
1352
     * @param string $propertyName
1353
     * @param mixed  $value
1354
     *
1355
     * @return bool
1356
     */
1357
    private function checkUniqueness(array $property, $propertyName, $value)
1358
    {
1359
        if (static::totalRecords([$propertyName => $value]) > 0) {
1360
            $this->getErrors()->push([
1361
                'error' => self::ERROR_NOT_UNIQUE,
1362
                'params' => [
1363
                    'field' => $propertyName,
1364
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1365
1366
            return false;
1367
        }
1368
1369
        return true;
1370
    }
1371
1372
    /**
1373
     * Gets the marshaled default value for a property (if set).
1374
     *
1375
     * @param string $property
1376
     *
1377
     * @return mixed
1378
     */
1379
    private function getPropertyDefault(array $property)
1380
    {
1381
        return array_value($property, 'default');
1382
    }
1383
}
1384