Completed
Push — master ( 9a3578...51f44a )
by Jared
02:42
created

Model::updated()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use BadMethodCallException;
15
use ICanBoogie\Inflector;
16
use Pimple\Container;
17
use Pulsar\Driver\DriverInterface;
18
use Pulsar\Exception\DriverMissingException;
19
use Pulsar\Exception\MassAssignmentException;
20
use Pulsar\Exception\ModelNotFoundException;
21
use Pulsar\Relation\BelongsTo;
22
use Pulsar\Relation\BelongsToMany;
23
use Pulsar\Relation\HasMany;
24
use Pulsar\Relation\HasOne;
25
use Symfony\Component\EventDispatcher\EventDispatcher;
26
27
/**
28
 * Class Model.
29
 */
30
abstract class Model implements \ArrayAccess
31
{
32
    const IMMUTABLE = 0;
33
    const MUTABLE_CREATE_ONLY = 1;
34
    const MUTABLE = 2;
35
36
    const TYPE_STRING = 'string';
37
    const TYPE_NUMBER = 'number'; // DEPRECATED
38
    const TYPE_INTEGER = 'integer';
39
    const TYPE_FLOAT = 'float';
40
    const TYPE_BOOLEAN = 'boolean';
41
    const TYPE_DATE = 'date';
42
    const TYPE_OBJECT = 'object';
43
    const TYPE_ARRAY = 'array';
44
45
    const ERROR_REQUIRED_FIELD_MISSING = 'required_field_missing';
46
    const ERROR_VALIDATION_FAILED = 'validation_failed';
47
    const ERROR_NOT_UNIQUE = 'not_unique';
48
49
    const DEFAULT_ID_PROPERTY = 'id';
50
51
    /////////////////////////////
52
    // Model visible variables
53
    /////////////////////////////
54
55
    /**
56
     * List of model ID property names.
57
     *
58
     * @var array
59
     */
60
    protected static $ids = [self::DEFAULT_ID_PROPERTY];
61
62
    /**
63
     * Property definitions expressed as a key-value map with
64
     * property names as the keys.
65
     * i.e. ['enabled' => ['type' => Model::TYPE_BOOLEAN]].
66
     *
67
     * @var array
68
     */
69
    protected static $properties = [];
70
71
    /**
72
     * @var Container
73
     */
74
    protected static $injectedApp;
75
76
    /**
77
     * @var array
78
     */
79
    protected static $dispatchers;
80
81
    /**
82
     * @var number|string|false
83
     */
84
    protected $_id;
85
86
    /**
87
     * @var array
88
     */
89
    protected $_ids;
90
91
    /**
92
     * @var Container
93
     */
94
    protected $app;
95
96
    /**
97
     * @var array
98
     */
99
    protected $_values = [];
100
101
    /**
102
     * @var array
103
     */
104
    protected $_unsaved = [];
105
106
    /**
107
     * @var bool
108
     */
109
    protected $_persisted = false;
110
111
    /**
112
     * @var array
113
     */
114
    protected $_relationships = [];
115
116
    /////////////////////////////
117
    // Base model variables
118
    /////////////////////////////
119
120
    /**
121
     * @var array
122
     */
123
    private static $propertyDefinitionBase = [
124
        'type' => self::TYPE_STRING,
125
        'mutable' => self::MUTABLE,
126
        'null' => false,
127
        'unique' => false,
128
        'required' => false,
129
    ];
130
131
    /**
132
     * @var array
133
     */
134
    private static $defaultIDProperty = [
135
        'type' => self::TYPE_INTEGER,
136
        'mutable' => self::IMMUTABLE,
137
    ];
138
139
    /**
140
     * @var array
141
     */
142
    private static $timestampProperties = [
143
        'created_at' => [
144
            'type' => self::TYPE_DATE,
145
            'validate' => 'timestamp|db_timestamp',
146
        ],
147
        'updated_at' => [
148
            'type' => self::TYPE_DATE,
149
            'validate' => 'timestamp|db_timestamp',
150
        ],
151
    ];
152
153
    /**
154
     * @var array
155
     */
156
    private static $initialized = [];
157
158
    /**
159
     * @var DriverInterface
160
     */
161
    private static $driver;
162
163
    /**
164
     * @var array
165
     */
166
    private static $accessors = [];
167
168
    /**
169
     * @var array
170
     */
171
    private static $mutators = [];
172
173
    /**
174
     * @var bool
175
     */
176
    private $_ignoreUnsaved;
177
178
    /**
179
     * Creates a new model object.
180
     *
181
     * @param array|string|Model|false $id     ordered array of ids or comma-separated id string
182
     * @param array                    $values optional key-value map to pre-seed model
183
     */
184
    public function __construct($id = false, array $values = [])
185
    {
186
        // initialize the model
187
        $this->app = self::$injectedApp;
188
        $this->init();
189
190
        // parse the supplied model ID
191
        if (is_array($id)) {
192
            // A model can be supplied as a primary key
193
            foreach ($id as &$el) {
194
                if ($el instanceof self) {
195
                    $el = $el->id();
196
                }
197
            }
198
199
            // The IDs come in as the same order as ::$ids.
200
            // We need to match up the elements on that
201
            // input into a key-value map for each ID property.
202
            $ids = [];
203
            $idQueue = array_reverse($id);
204
            foreach (static::$ids as $k => $f) {
205
                $ids[$f] = (count($idQueue) > 0) ? array_pop($idQueue) : false;
206
            }
207
208
            $this->_id = implode(',', $id);
209
            $this->_ids = $ids;
210
        } elseif ($id instanceof self) {
211
            // A model can be supplied as a primary key
212
            $this->_id = $id->id();
213
            $this->_ids = $id->ids();
214
        } else {
215
            $this->_id = $id;
216
            $idProperty = static::$ids[0];
217
            $this->_ids = [$idProperty => $id];
218
        }
219
220
        // load any given values
221
        if (count($values) > 0) {
222
            $this->refreshWith($values);
223
        }
224
    }
225
226
    /**
227
     * Performs initialization on this model.
228
     */
229
    private function init()
230
    {
231
        // ensure the initialize function is called only once
232
        $k = get_called_class();
233
        if (!isset(self::$initialized[$k])) {
234
            $this->initialize();
235
            self::$initialized[$k] = true;
236
        }
237
    }
238
239
    /**
240
     * The initialize() method is called once per model. It's used
241
     * to perform any one-off tasks before the model gets
242
     * constructed. This is a great place to add any model
243
     * properties. When extending this method be sure to call
244
     * parent::initialize() as some important stuff happens here.
245
     * If extending this method to add properties then you should
246
     * call parent::initialize() after adding any properties.
247
     */
248
    protected function initialize()
249
    {
250
        // load the driver
251
        static::getDriver();
252
253
        // add in the default ID property
254
        if (static::$ids == [self::DEFAULT_ID_PROPERTY] && !isset(static::$properties[self::DEFAULT_ID_PROPERTY])) {
255
            static::$properties[self::DEFAULT_ID_PROPERTY] = self::$defaultIDProperty;
256
        }
257
258
        // generates created_at and updated_at timestamps
259
        if (property_exists($this, 'autoTimestamps')) {
260
            $this->installAutoTimestamps();
261
        }
262
263
        // fill in each property by extending the property
264
        // definition base
265
        foreach (static::$properties as &$property) {
266
            $property = array_replace(self::$propertyDefinitionBase, $property);
267
        }
268
269
        // order the properties array by name for consistency
270
        // since it is constructed in a random order
271
        ksort(static::$properties);
272
    }
273
274
    /**
275
     * Installs the `created_at` and `updated_at` properties.
276
     */
277
    private function installAutoTimestamps()
278
    {
279
        static::$properties = array_replace(self::$timestampProperties, static::$properties);
280
281
        self::creating(function (ModelEvent $event) {
282
            $model = $event->getModel();
283
            $model->created_at = time();
0 ignored issues
show
Documentation introduced by
The property created_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
284
            $model->updated_at = time();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
285
        });
286
287
        self::updating(function (ModelEvent $event) {
288
            $event->getModel()->updated_at = time();
0 ignored issues
show
Documentation introduced by
The property updated_at does not exist on object<Pulsar\Model>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
289
        });
290
    }
291
292
    /**
293
     * Injects a DI container.
294
     *
295
     * @param Container $app
296
     */
297
    public static function inject(Container $app)
298
    {
299
        self::$injectedApp = $app;
300
    }
301
302
    /**
303
     * Gets the DI container used for this model.
304
     *
305
     * @return Container
306
     */
307
    public function getApp()
308
    {
309
        return $this->app;
310
    }
311
312
    /**
313
     * Sets the driver for all models.
314
     *
315
     * @param DriverInterface $driver
316
     */
317
    public static function setDriver(DriverInterface $driver)
318
    {
319
        self::$driver = $driver;
320
    }
321
322
    /**
323
     * Gets the driver for all models.
324
     *
325
     * @return DriverInterface
326
     *
327
     * @throws DriverMissingException when a driver has not been set yet
328
     */
329
    public static function getDriver()
330
    {
331
        if (!self::$driver) {
332
            throw new DriverMissingException('A model driver has not been set yet.');
333
        }
334
335
        return self::$driver;
336
    }
337
338
    /**
339
     * Clears the driver for all models.
340
     */
341
    public static function clearDriver()
342
    {
343
        self::$driver = null;
344
    }
345
346
    /**
347
     * Gets the name of the model, i.e. User.
348
     *
349
     * @return string
350
     */
351
    public static function modelName()
352
    {
353
        // strip namespacing
354
        $paths = explode('\\', get_called_class());
355
356
        return end($paths);
357
    }
358
359
    /**
360
     * Gets the model ID.
361
     *
362
     * @return string|number|false ID
363
     */
364
    public function id()
365
    {
366
        return $this->_id;
367
    }
368
369
    /**
370
     * Gets a key-value map of the model ID.
371
     *
372
     * @return array ID map
373
     */
374
    public function ids()
375
    {
376
        return $this->_ids;
377
    }
378
379
    /////////////////////////////
380
    // Magic Methods
381
    /////////////////////////////
382
383
    /**
384
     * Converts the model into a string.
385
     *
386
     * @return string
387
     */
388
    public function __toString()
389
    {
390
        return get_called_class().'('.$this->_id.')';
391
    }
392
393
    /**
394
     * Shortcut to a get() call for a given property.
395
     *
396
     * @param string $name
397
     *
398
     * @return mixed
399
     */
400
    public function __get($name)
401
    {
402
        $result = $this->get([$name]);
403
404
        return reset($result);
405
    }
406
407
    /**
408
     * Sets an unsaved value.
409
     *
410
     * @param string $name
411
     * @param mixed  $value
412
     */
413
    public function __set($name, $value)
414
    {
415
        // if changing property, remove relation model
416
        if (isset($this->_relationships[$name])) {
417
            unset($this->_relationships[$name]);
418
        }
419
420
        // call any mutators
421
        $mutator = self::getMutator($name);
422
        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...
423
            $this->_unsaved[$name] = $this->$mutator($value);
424
        } else {
425
            $this->_unsaved[$name] = $value;
426
        }
427
    }
428
429
    /**
430
     * Checks if an unsaved value or property exists by this name.
431
     *
432
     * @param string $name
433
     *
434
     * @return bool
435
     */
436
    public function __isset($name)
437
    {
438
        return array_key_exists($name, $this->_unsaved) || static::hasProperty($name);
439
    }
440
441
    /**
442
     * Unsets an unsaved value.
443
     *
444
     * @param string $name
445
     */
446
    public function __unset($name)
447
    {
448
        if (array_key_exists($name, $this->_unsaved)) {
449
            // if changing property, remove relation model
450
            if (isset($this->_relationships[$name])) {
451
                unset($this->_relationships[$name]);
452
            }
453
454
            unset($this->_unsaved[$name]);
455
        }
456
    }
457
458
    /////////////////////////////
459
    // ArrayAccess Interface
460
    /////////////////////////////
461
462
    public function offsetExists($offset)
463
    {
464
        return isset($this->$offset);
465
    }
466
467
    public function offsetGet($offset)
468
    {
469
        return $this->$offset;
470
    }
471
472
    public function offsetSet($offset, $value)
473
    {
474
        $this->$offset = $value;
475
    }
476
477
    public function offsetUnset($offset)
478
    {
479
        unset($this->$offset);
480
    }
481
482
    public static function __callStatic($name, $parameters)
483
    {
484
        // Any calls to unkown static methods should be deferred to
485
        // the query. This allows calls like User::where()
486
        // 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...
487
        return call_user_func_array([static::query(), $name], $parameters);
488
    }
489
490
    /////////////////////////////
491
    // Property Definitions
492
    /////////////////////////////
493
494
    /**
495
     * Gets all the property definitions for the model.
496
     *
497
     * @return array key-value map of properties
498
     */
499
    public static function getProperties()
500
    {
501
        return static::$properties;
502
    }
503
504
    /**
505
     * Gets a property defition for the model.
506
     *
507
     * @param string $property property to lookup
508
     *
509
     * @return array|null property
510
     */
511
    public static function getProperty($property)
512
    {
513
        return array_value(static::$properties, $property);
514
    }
515
516
    /**
517
     * Gets the names of the model ID properties.
518
     *
519
     * @return array
520
     */
521
    public static function getIDProperties()
522
    {
523
        return static::$ids;
524
    }
525
526
    /**
527
     * Checks if the model has a property.
528
     *
529
     * @param string $property property
530
     *
531
     * @return bool has property
532
     */
533
    public static function hasProperty($property)
534
    {
535
        return isset(static::$properties[$property]);
536
    }
537
538
    /**
539
     * Gets the mutator method name for a given proeprty name.
540
     * Looks for methods in the form of `setPropertyValue`.
541
     * i.e. the mutator for `last_name` would be `setLastNameValue`.
542
     *
543
     * @param string $property property
544
     *
545
     * @return string|false method name if it exists
546
     */
547 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...
548
    {
549
        $class = get_called_class();
550
551
        $k = $class.':'.$property;
552
        if (!array_key_exists($k, self::$mutators)) {
553
            $inflector = Inflector::get();
554
            $method = 'set'.$inflector->camelize($property).'Value';
555
556
            if (!method_exists($class, $method)) {
557
                $method = false;
558
            }
559
560
            self::$mutators[$k] = $method;
561
        }
562
563
        return self::$mutators[$k];
564
    }
565
566
    /**
567
     * Gets the accessor method name for a given proeprty name.
568
     * Looks for methods in the form of `getPropertyValue`.
569
     * i.e. the accessor for `last_name` would be `getLastNameValue`.
570
     *
571
     * @param string $property property
572
     *
573
     * @return string|false method name if it exists
574
     */
575 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...
576
    {
577
        $class = get_called_class();
578
579
        $k = $class.':'.$property;
580
        if (!array_key_exists($k, self::$accessors)) {
581
            $inflector = Inflector::get();
582
            $method = 'get'.$inflector->camelize($property).'Value';
583
584
            if (!method_exists($class, $method)) {
585
                $method = false;
586
            }
587
588
            self::$accessors[$k] = $method;
589
        }
590
591
        return self::$accessors[$k];
592
    }
593
594
    /////////////////////////////
595
    // CRUD Operations
596
    /////////////////////////////
597
598
    /**
599
     * Saves the model.
600
     *
601
     * @return bool true when the operation was successful
602
     */
603
    public function save()
604
    {
605
        if ($this->_id === false) {
606
            return $this->create();
607
        }
608
609
        return $this->set();
610
    }
611
612
    /**
613
     * Creates a new model.
614
     *
615
     * @param array $data optional key-value properties to set
616
     *
617
     * @return bool true when the operation was successful
618
     *
619
     * @throws BadMethodCallException when called on an existing model
620
     */
621
    public function create(array $data = [])
622
    {
623
        if ($this->_id !== false) {
624
            throw new BadMethodCallException('Cannot call create() on an existing model');
625
        }
626
627
        // mass assign values passed into create()
628
        $this->setValues($data);
629
630
        // dispatch the model.creating event
631
        if (!$this->handleDispatch(ModelEvent::CREATING)) {
632
            return false;
633
        }
634
635
        $requiredProperties = [];
636
        foreach (static::$properties as $name => $property) {
637
            // build a list of the required properties
638
            if ($property['required']) {
639
                $requiredProperties[] = $name;
640
            }
641
642
            // add in default values
643
            if (!array_key_exists($name, $this->_unsaved) && array_key_exists('default', $property)) {
644
                $this->_unsaved[$name] = $property['default'];
645
            }
646
        }
647
648
        // validate the values being saved
649
        $validated = true;
650
        $insertArray = [];
651
        foreach ($this->_unsaved as $name => $value) {
652
            // exclude if value does not map to a property
653
            if (!isset(static::$properties[$name])) {
654
                continue;
655
            }
656
657
            $property = static::$properties[$name];
658
659
            // cannot insert immutable values
660
            // (unless using the default value)
661
            if ($property['mutable'] == self::IMMUTABLE && $value !== $this->getPropertyDefault($property)) {
662
                continue;
663
            }
664
665
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
666
            $insertArray[$name] = $value;
667
        }
668
669
        // check for required fields
670
        foreach ($requiredProperties as $name) {
671
            if (!isset($insertArray[$name])) {
672
                $property = static::$properties[$name];
673
                $this->app['errors']->push([
674
                    'error' => self::ERROR_REQUIRED_FIELD_MISSING,
675
                    'params' => [
676
                        'field' => $name,
677
                        'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($name), ], ]);
678
679
                $validated = false;
680
            }
681
        }
682
683
        if (!$validated) {
684
            return false;
685
        }
686
687
        $created = self::$driver->createModel($this, $insertArray);
688
689
        if ($created) {
690
            // determine the model's new ID
691
            $this->getNewID();
692
693
            // NOTE clear the local cache before the model.created
694
            // event so that fetching values forces a reload
695
            // from the storage layer
696
            $this->clearCache();
697
698
            // dispatch the model.created event
699
            if (!$this->handleDispatch(ModelEvent::CREATED)) {
700
                return false;
701
            }
702
        }
703
704
        return $created;
705
    }
706
707
    /**
708
     * Ignores unsaved values when fetching the next value.
709
     *
710
     * @return self
711
     */
712
    public function ignoreUnsaved()
713
    {
714
        $this->_ignoreUnsaved = true;
715
716
        return $this;
717
    }
718
719
    /**
720
     * Fetches property values from the model.
721
     *
722
     * This method looks up values in this order:
723
     * IDs, local cache, unsaved values, storage layer, defaults
724
     *
725
     * @param array $properties list of property names to fetch values of
726
     *
727
     * @return array
728
     */
729
    public function get(array $properties)
730
    {
731
        // load the values from the IDs and local model cache
732
        $values = array_replace($this->ids(), $this->_values);
733
734
        // unless specified, use any unsaved values
735
        $ignoreUnsaved = $this->_ignoreUnsaved;
736
        $this->_ignoreUnsaved = false;
737
738
        if (!$ignoreUnsaved) {
739
            $values = array_replace($values, $this->_unsaved);
740
        }
741
742
        // attempt to load any missing values from the storage layer
743
        $numMissing = count(array_diff($properties, array_keys($values)));
744
        if ($numMissing > 0) {
745
            $this->refresh();
746
            $values = array_replace($values, $this->_values);
747
748
            if (!$ignoreUnsaved) {
749
                $values = array_replace($values, $this->_unsaved);
750
            }
751
        }
752
753
        // build a key-value map of the requested properties
754
        $return = [];
755
        foreach ($properties as $k) {
756
            if (array_key_exists($k, $values)) {
757
                $return[$k] = $values[$k];
758
            // set any missing values to the default value
759
            } elseif (static::hasProperty($k)) {
760
                $return[$k] = $this->_values[$k] = $this->getPropertyDefault(static::$properties[$k]);
761
            // use null for values of non-properties
762
            } else {
763
                $return[$k] = null;
764
            }
765
766
            // call any accessors
767
            if ($accessor = self::getAccessor($k)) {
768
                $return[$k] = $this->$accessor($return[$k]);
769
            }
770
        }
771
772
        return $return;
773
    }
774
775
    /**
776
     * Populates a newly created model with its ID.
777
     */
778
    protected function getNewID()
779
    {
780
        $ids = [];
781
        $namedIds = [];
782
        foreach (static::$ids as $k) {
783
            // attempt use the supplied value if the ID property is mutable
784
            $property = static::getProperty($k);
785
            if (in_array($property['mutable'], [self::MUTABLE, self::MUTABLE_CREATE_ONLY]) && isset($this->_unsaved[$k])) {
786
                $id = $this->_unsaved[$k];
787
            } else {
788
                $id = self::$driver->getCreatedID($this, $k);
789
            }
790
791
            $ids[] = $id;
792
            $namedIds[$k] = $id;
793
        }
794
795
        $this->_id = implode(',', $ids);
796
        $this->_ids = $namedIds;
797
    }
798
799
    /**
800
     * Sets a collection values on the model from an untrusted input.
801
     *
802
     * @param array $values
803
     *
804
     * @throws MassAssignmentException when assigning a value that is protected or not whitelisted
805
     *
806
     * @return self
807
     */
808
    public function setValues($values)
809
    {
810
        // check if the model has a mass assignment whitelist
811
        $permitted = (property_exists($this, 'permitted')) ? static::$permitted : false;
812
813
        // if no whitelist, then check for a blacklist
814
        $protected = (!is_array($permitted) && property_exists($this, 'protected')) ? static::$protected : false;
815
816
        foreach ($values as $k => $value) {
817
            // check for mass assignment violations
818
            if (($permitted && !in_array($k, $permitted)) ||
819
                ($protected && in_array($k, $protected))) {
820
                throw new MassAssignmentException("Mass assignment of $k on ".static::modelName().' is not allowed');
821
            }
822
823
            $this->$k = $value;
824
        }
825
826
        return $this;
827
    }
828
829
    /**
830
     * Converts the model to an array.
831
     *
832
     * @return array
833
     */
834
    public function toArray()
835
    {
836
        // build the list of properties to retrieve
837
        $properties = array_keys(static::$properties);
838
839
        // remove any hidden properties
840
        $hide = (property_exists($this, 'hidden')) ? static::$hidden : [];
841
        $properties = array_diff($properties, $hide);
842
843
        // add any appended properties
844
        $append = (property_exists($this, 'appended')) ? static::$appended : [];
845
        $properties = array_merge($properties, $append);
846
847
        // get the values for the properties
848
        $result = $this->get($properties);
849
850
        foreach ($result as $k => &$value) {
851
            // convert any models to arrays
852
            if ($value instanceof self) {
853
                $value = $value->toArray();
854
            }
855
        }
856
857
        // DEPRECATED
858
        // apply the transformation hook
859
        if (method_exists($this, 'toArrayHook')) {
860
            $this->toArrayHook($result, [], [], []);
0 ignored issues
show
Bug introduced by
The method toArrayHook() does not exist on Pulsar\Model. Did you maybe mean toArray()?

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

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

Loading history...
861
        }
862
863
        return $result;
864
    }
865
866
    /**
867
     * Updates the model.
868
     *
869
     * @param array $data optional key-value properties to set
870
     *
871
     * @return bool true when the operation was successful
872
     *
873
     * @throws BadMethodCallException when not called on an existing model
874
     */
875
    public function set(array $data = [])
876
    {
877
        if ($this->_id === false) {
878
            throw new BadMethodCallException('Can only call set() on an existing model');
879
        }
880
881
        // mass assign values passed into set()
882
        $this->setValues($data);
883
884
        // not updating anything?
885
        if (count($this->_unsaved) == 0) {
886
            return true;
887
        }
888
889
        // dispatch the model.updating event
890
        if (!$this->handleDispatch(ModelEvent::UPDATING)) {
891
            return false;
892
        }
893
894
        // DEPRECATED
895
        if (method_exists($this, 'preSetHook') && !$this->preSetHook($this->_unsaved)) {
0 ignored issues
show
Bug introduced by
The method preSetHook() does not seem to exist on object<Pulsar\Model>.

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

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

Loading history...
896
            return false;
897
        }
898
899
        // validate the values being saved
900
        $validated = true;
901
        $updateArray = [];
902
        foreach ($this->_unsaved as $name => $value) {
903
            // exclude if value does not map to a property
904
            if (!isset(static::$properties[$name])) {
905
                continue;
906
            }
907
908
            $property = static::$properties[$name];
909
910
            // can only modify mutable properties
911
            if ($property['mutable'] != self::MUTABLE) {
912
                continue;
913
            }
914
915
            $validated = $validated && $this->filterAndValidate($property, $name, $value);
916
            $updateArray[$name] = $value;
917
        }
918
919
        if (!$validated) {
920
            return false;
921
        }
922
923
        $updated = self::$driver->updateModel($this, $updateArray);
924
925
        if ($updated) {
926
            // NOTE clear the local cache before the model.updated
927
            // event so that fetching values forces a reload
928
            // from the storage layer
929
            $this->clearCache();
930
931
            // dispatch the model.updated event
932
            if (!$this->handleDispatch(ModelEvent::UPDATED)) {
933
                return false;
934
            }
935
        }
936
937
        return $updated;
938
    }
939
940
    /**
941
     * Delete the model.
942
     *
943
     * @return bool true when the operation was successful
944
     */
945
    public function delete()
946
    {
947
        if ($this->_id === false) {
948
            throw new BadMethodCallException('Can only call delete() on an existing model');
949
        }
950
951
        // dispatch the model.deleting event
952
        if (!$this->handleDispatch(ModelEvent::DELETING)) {
953
            return false;
954
        }
955
956
        $deleted = self::$driver->deleteModel($this);
957
958
        if ($deleted) {
959
            // dispatch the model.deleted event
960
            if (!$this->handleDispatch(ModelEvent::DELETED)) {
961
                return false;
962
            }
963
964
            $this->_persisted = false;
965
        }
966
967
        return $deleted;
968
    }
969
970
    /////////////////////////////
971
    // Queries
972
    /////////////////////////////
973
974
    /**
975
     * Generates a new query instance.
976
     *
977
     * @return Query
978
     */
979
    public static function query()
980
    {
981
        // Create a new model instance for the query to ensure
982
        // that the model's initialize() method gets called.
983
        // Otherwise, the property definitions will be incomplete.
984
        $model = new static();
985
986
        return new Query($model);
987
    }
988
989
    /**
990
     * Finds a single instance of a model given it's ID.
991
     *
992
     * @param mixed $id
993
     *
994
     * @return Model|false
995
     */
996
    public static function find($id)
997
    {
998
        $model = new static($id);
999
        $values = self::getDriver()->loadModel($model);
1000
        if (!is_array($values)) {
1001
            return false;
1002
        }
1003
1004
        return $model->refreshWith($values);
1005
    }
1006
1007
    /**
1008
     * Finds a single instance of a model given it's ID or throws an exception.
1009
     *
1010
     * @param mixed $id
1011
     *
1012
     * @return Model
1013
     *
1014
     * @throws ModelNotFoundException when a model could not be found
1015
     */
1016
    public static function findOrFail($id)
1017
    {
1018
        $model = static::find($id);
1019
        if (!$model) {
1020
            throw new ModelNotFoundException('Could not find the requested '.static::modelName());
1021
        }
1022
1023
        return $model;
1024
    }
1025
1026
    /**
1027
     * @deprecated
1028
     *
1029
     * Checks if the model exists in the database
1030
     *
1031
     * @return bool
1032
     */
1033
    public function exists()
1034
    {
1035
        return static::where($this->ids())->count() == 1;
1036
    }
1037
1038
    /**
1039
     * Tells if this model instance has been persisted to the data layer.
1040
     *
1041
     * NOTE: this does not actually perform a check with the data layer
1042
     *
1043
     * @return bool
1044
     */
1045
    public function persisted()
1046
    {
1047
        return $this->_persisted;
1048
    }
1049
1050
    /**
1051
     * Loads the model from the storage layer.
1052
     *
1053
     * @return self
1054
     */
1055
    public function refresh()
1056
    {
1057
        if ($this->_id === false) {
1058
            return $this;
1059
        }
1060
1061
        $values = self::$driver->loadModel($this);
1062
1063
        if (!is_array($values)) {
1064
            return $this;
1065
        }
1066
1067
        // clear any relations
1068
        $this->_relationships = [];
1069
1070
        return $this->refreshWith($values);
1071
    }
1072
1073
    /**
1074
     * Loads values into the model.
1075
     *
1076
     * @param array $values values
1077
     *
1078
     * @return self
1079
     */
1080
    public function refreshWith(array $values)
1081
    {
1082
        $this->_persisted = true;
1083
        $this->_values = $values;
1084
1085
        return $this;
1086
    }
1087
1088
    /**
1089
     * Clears the cache for this model.
1090
     *
1091
     * @return self
1092
     */
1093
    public function clearCache()
1094
    {
1095
        $this->_unsaved = [];
1096
        $this->_values = [];
1097
        $this->_relationships = [];
1098
1099
        return $this;
1100
    }
1101
1102
    /////////////////////////////
1103
    // Relationships
1104
    /////////////////////////////
1105
1106
    /**
1107
     * @deprecated
1108
     *
1109
     * Gets the model for a has-one relationship
1110
     *
1111
     * @param string $k property
1112
     *
1113
     * @return Model|null
1114
     */
1115
    public function relation($k)
1116
    {
1117
        $id = $this->$k;
1118
        if (!$id) {
1119
            return;
1120
        }
1121
1122
        if (!isset($this->_relationships[$k])) {
1123
            $property = static::getProperty($k);
1124
            $relationModelClass = $property['relation'];
1125
            $this->_relationships[$k] = $relationModelClass::find($id);
1126
        }
1127
1128
        return $this->_relationships[$k];
1129
    }
1130
1131
    /**
1132
     * @deprecated
1133
     *
1134
     * Sets the model for a has-one relationship
1135
     *
1136
     * @param string $k
1137
     * @param Model  $model
1138
     *
1139
     * @return self
1140
     */
1141
    public function setRelation($k, Model $model)
1142
    {
1143
        $this->$k = $model->id();
1144
        $this->_relationships[$k] = $model;
1145
1146
        return $this;
1147
    }
1148
1149
    /**
1150
     * Creates the parent side of a One-To-One relationship.
1151
     *
1152
     * @param string $model      foreign model class
1153
     * @param string $foreignKey identifying key on foreign model
1154
     * @param string $localKey   identifying key on local model
1155
     *
1156
     * @return Relation\Relation
1157
     */
1158 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...
1159
    {
1160
        // the default local key would look like `user_id`
1161
        // for a model named User
1162
        if (!$foreignKey) {
1163
            $inflector = Inflector::get();
1164
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1165
        }
1166
1167
        if (!$localKey) {
1168
            $localKey = self::DEFAULT_ID_PROPERTY;
1169
        }
1170
1171
        return new HasOne($model, $foreignKey, $localKey, $this);
1172
    }
1173
1174
    /**
1175
     * Creates the child side of a One-To-One or One-To-Many relationship.
1176
     *
1177
     * @param string $model      foreign model class
1178
     * @param string $foreignKey identifying key on foreign model
1179
     * @param string $localKey   identifying key on local model
1180
     *
1181
     * @return Relation\Relation
1182
     */
1183 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...
1184
    {
1185
        if (!$foreignKey) {
1186
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1187
        }
1188
1189
        // the default local key would look like `user_id`
1190
        // for a model named User
1191
        if (!$localKey) {
1192
            $inflector = Inflector::get();
1193
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1194
        }
1195
1196
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1197
    }
1198
1199
    /**
1200
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1201
     *
1202
     * @param string $model      foreign model class
1203
     * @param string $foreignKey identifying key on foreign model
1204
     * @param string $localKey   identifying key on local model
1205
     *
1206
     * @return Relation\Relation
1207
     */
1208 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...
1209
    {
1210
        // the default local key would look like `user_id`
1211
        // for a model named User
1212
        if (!$foreignKey) {
1213
            $inflector = Inflector::get();
1214
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1215
        }
1216
1217
        if (!$localKey) {
1218
            $localKey = self::DEFAULT_ID_PROPERTY;
1219
        }
1220
1221
        return new HasMany($model, $foreignKey, $localKey, $this);
1222
    }
1223
1224
    /**
1225
     * Creates the child side of a Many-To-Many relationship.
1226
     *
1227
     * @param string $model      foreign model class
1228
     * @param string $foreignKey identifying key on foreign model
1229
     * @param string $localKey   identifying key on local model
1230
     *
1231
     * @return Relation\Relation
1232
     */
1233 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...
1234
    {
1235
        if (!$foreignKey) {
1236
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1237
        }
1238
1239
        // the default local key would look like `user_id`
1240
        // for a model named User
1241
        if (!$localKey) {
1242
            $inflector = Inflector::get();
1243
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1244
        }
1245
1246
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1247
    }
1248
1249
    /////////////////////////////
1250
    // Events
1251
    /////////////////////////////
1252
1253
    /**
1254
     * Gets the event dispatcher.
1255
     *
1256
     * @return EventDispatcher
1257
     */
1258
    public static function getDispatcher($ignoreCache = false)
1259
    {
1260
        $class = get_called_class();
1261
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1262
            self::$dispatchers[$class] = new EventDispatcher();
1263
        }
1264
1265
        return self::$dispatchers[$class];
1266
    }
1267
1268
    /**
1269
     * Subscribes to a listener to an event.
1270
     *
1271
     * @param string   $event    event name
1272
     * @param callable $listener
1273
     * @param int      $priority optional priority, higher #s get called first
1274
     */
1275
    public static function listen($event, callable $listener, $priority = 0)
1276
    {
1277
        static::getDispatcher()->addListener($event, $listener, $priority);
1278
    }
1279
1280
    /**
1281
     * Adds a listener to the model.creating and model.updating events.
1282
     *
1283
     * @param callable $listener
1284
     * @param int      $priority
1285
     */
1286
    public static function saving(callable $listener, $priority = 0)
1287
    {
1288
        static::listen(ModelEvent::CREATING, $listener, $priority);
1289
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1290
    }
1291
1292
    /**
1293
     * Adds a listener to the model.created and model.updated events.
1294
     *
1295
     * @param callable $listener
1296
     * @param int      $priority
1297
     */
1298
    public static function saved(callable $listener, $priority = 0)
1299
    {
1300
        static::listen(ModelEvent::CREATED, $listener, $priority);
1301
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1302
    }
1303
1304
    /**
1305
     * Adds a listener to the model.creating event.
1306
     *
1307
     * @param callable $listener
1308
     * @param int      $priority
1309
     */
1310
    public static function creating(callable $listener, $priority = 0)
1311
    {
1312
        static::listen(ModelEvent::CREATING, $listener, $priority);
1313
    }
1314
1315
    /**
1316
     * Adds a listener to the model.created event.
1317
     *
1318
     * @param callable $listener
1319
     * @param int      $priority
1320
     */
1321
    public static function created(callable $listener, $priority = 0)
1322
    {
1323
        static::listen(ModelEvent::CREATED, $listener, $priority);
1324
    }
1325
1326
    /**
1327
     * Adds a listener to the model.updating event.
1328
     *
1329
     * @param callable $listener
1330
     * @param int      $priority
1331
     */
1332
    public static function updating(callable $listener, $priority = 0)
1333
    {
1334
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1335
    }
1336
1337
    /**
1338
     * Adds a listener to the model.updated event.
1339
     *
1340
     * @param callable $listener
1341
     * @param int      $priority
1342
     */
1343
    public static function updated(callable $listener, $priority = 0)
1344
    {
1345
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1346
    }
1347
1348
    /**
1349
     * Adds a listener to the model.deleting event.
1350
     *
1351
     * @param callable $listener
1352
     * @param int      $priority
1353
     */
1354
    public static function deleting(callable $listener, $priority = 0)
1355
    {
1356
        static::listen(ModelEvent::DELETING, $listener, $priority);
1357
    }
1358
1359
    /**
1360
     * Adds a listener to the model.deleted event.
1361
     *
1362
     * @param callable $listener
1363
     * @param int      $priority
1364
     */
1365
    public static function deleted(callable $listener, $priority = 0)
1366
    {
1367
        static::listen(ModelEvent::DELETED, $listener, $priority);
1368
    }
1369
1370
    /**
1371
     * Dispatches an event.
1372
     *
1373
     * @param string $eventName
1374
     *
1375
     * @return ModelEvent
1376
     */
1377
    protected function dispatch($eventName)
1378
    {
1379
        $event = new ModelEvent($this);
1380
1381
        return static::getDispatcher()->dispatch($eventName, $event);
1382
    }
1383
1384
    /**
1385
     * Dispatches the given event and checks if it was successful.
1386
     *
1387
     * @param string $eventName
1388
     *
1389
     * @return bool true if the events were successfully propagated
1390
     */
1391
    private function handleDispatch($eventName)
1392
    {
1393
        $event = $this->dispatch($eventName);
1394
1395
        return !$event->isPropagationStopped();
1396
    }
1397
1398
    /////////////////////////////
1399
    // Validation
1400
    /////////////////////////////
1401
1402
    /**
1403
     * Validates and marshals a value to storage.
1404
     *
1405
     * @param array  $property
1406
     * @param string $propertyName
1407
     * @param mixed  $value
1408
     *
1409
     * @return bool
1410
     */
1411
    private function filterAndValidate(array $property, $propertyName, &$value)
1412
    {
1413
        // assume empty string is a null value for properties
1414
        // that are marked as optionally-null
1415
        if ($property['null'] && empty($value)) {
1416
            $value = null;
1417
1418
            return true;
1419
        }
1420
1421
        // validate
1422
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1423
1424
        // unique?
1425
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1426
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1427
        }
1428
1429
        return $valid;
1430
    }
1431
1432
    /**
1433
     * Validates a value for a property.
1434
     *
1435
     * @param array  $property
1436
     * @param string $propertyName
1437
     * @param mixed  $value
1438
     *
1439
     * @return array
1440
     */
1441
    private function validate(array $property, $propertyName, $value)
1442
    {
1443
        $valid = true;
1444
1445
        if (isset($property['validate']) && is_callable($property['validate'])) {
1446
            $valid = call_user_func_array($property['validate'], [$value]);
1447
        } elseif (isset($property['validate'])) {
1448
            $valid = Validate::is($value, $property['validate']);
0 ignored issues
show
Deprecated Code introduced by
The method Pulsar\Validate::is() has been deprecated with message: Validates one or more fields based upon certain filters. Filters may be chained and will be executed in order
i.e. Validate::is( '[email protected]', 'email' ) or Validate::is( ['password1', 'password2'], 'matching|password:8|required' ). NOTE: some filters may modify the data, which is passed in by reference

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

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

Loading history...
1449
        }
1450
1451 View Code Duplication
        if (!$valid) {
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...
1452
            $this->app['errors']->push([
1453
                'error' => self::ERROR_VALIDATION_FAILED,
1454
                'params' => [
1455
                    'field' => $propertyName,
1456
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1457
        }
1458
1459
        return [$valid, $value];
1460
    }
1461
1462
    /**
1463
     * Checks if a value is unique for a property.
1464
     *
1465
     * @param array  $property
1466
     * @param string $propertyName
1467
     * @param mixed  $value
1468
     *
1469
     * @return bool
1470
     */
1471
    private function checkUniqueness(array $property, $propertyName, $value)
1472
    {
1473
        $n = static::where([$propertyName => $value])->count();
1474 View Code Duplication
        if ($n > 0) {
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...
1475
            $this->app['errors']->push([
1476
                'error' => self::ERROR_NOT_UNIQUE,
1477
                'params' => [
1478
                    'field' => $propertyName,
1479
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1480
1481
            return false;
1482
        }
1483
1484
        return true;
1485
    }
1486
1487
    /**
1488
     * Gets the marshaled default value for a property (if set).
1489
     *
1490
     * @param string $property
1491
     *
1492
     * @return mixed
1493
     */
1494
    private function getPropertyDefault(array $property)
1495
    {
1496
        return array_value($property, 'default');
1497
    }
1498
}
1499