Completed
Push — master ( a90e9d...bdf610 )
by Jared
06:48
created

Model::totalRecords()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
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 object corresponding to a relation
1110
     * WARNING no check is used to see if the model returned actually exists
1111
     *
1112
     * @param string $propertyName property
1113
     *
1114
     * @return Model
1115
     */
1116
    public function relation($propertyName)
1117
    {
1118
        $property = static::getProperty($propertyName);
1119
1120
        if (!isset($this->_relationships[$propertyName])) {
1121
            $relationModelName = $property['relation'];
1122
            $this->_relationships[$propertyName] = new $relationModelName($this->$propertyName);
1123
        }
1124
1125
        return $this->_relationships[$propertyName];
1126
    }
1127
1128
    /**
1129
     * Creates the parent side of a One-To-One relationship.
1130
     *
1131
     * @param string $model      foreign model class
1132
     * @param string $foreignKey identifying key on foreign model
1133
     * @param string $localKey   identifying key on local model
1134
     *
1135
     * @return Relation\Relation
1136
     */
1137 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...
1138
    {
1139
        // the default local key would look like `user_id`
1140
        // for a model named User
1141
        if (!$foreignKey) {
1142
            $inflector = Inflector::get();
1143
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1144
        }
1145
1146
        if (!$localKey) {
1147
            $localKey = self::DEFAULT_ID_PROPERTY;
1148
        }
1149
1150
        return new HasOne($model, $foreignKey, $localKey, $this);
1151
    }
1152
1153
    /**
1154
     * Creates the child side of a One-To-One or One-To-Many relationship.
1155
     *
1156
     * @param string $model      foreign model class
1157
     * @param string $foreignKey identifying key on foreign model
1158
     * @param string $localKey   identifying key on local model
1159
     *
1160
     * @return Relation\Relation
1161
     */
1162 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...
1163
    {
1164
        if (!$foreignKey) {
1165
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1166
        }
1167
1168
        // the default local key would look like `user_id`
1169
        // for a model named User
1170
        if (!$localKey) {
1171
            $inflector = Inflector::get();
1172
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1173
        }
1174
1175
        return new BelongsTo($model, $foreignKey, $localKey, $this);
1176
    }
1177
1178
    /**
1179
     * Creates the parent side of a Many-To-One or Many-To-Many relationship.
1180
     *
1181
     * @param string $model      foreign model class
1182
     * @param string $foreignKey identifying key on foreign model
1183
     * @param string $localKey   identifying key on local model
1184
     *
1185
     * @return Relation\Relation
1186
     */
1187 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...
1188
    {
1189
        // the default local key would look like `user_id`
1190
        // for a model named User
1191
        if (!$foreignKey) {
1192
            $inflector = Inflector::get();
1193
            $foreignKey = strtolower($inflector->underscore(static::modelName())).'_id';
1194
        }
1195
1196
        if (!$localKey) {
1197
            $localKey = self::DEFAULT_ID_PROPERTY;
1198
        }
1199
1200
        return new HasMany($model, $foreignKey, $localKey, $this);
1201
    }
1202
1203
    /**
1204
     * Creates the child side of a Many-To-Many relationship.
1205
     *
1206
     * @param string $model      foreign model class
1207
     * @param string $foreignKey identifying key on foreign model
1208
     * @param string $localKey   identifying key on local model
1209
     *
1210
     * @return Relation\Relation
1211
     */
1212 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...
1213
    {
1214
        if (!$foreignKey) {
1215
            $foreignKey = self::DEFAULT_ID_PROPERTY;
1216
        }
1217
1218
        // the default local key would look like `user_id`
1219
        // for a model named User
1220
        if (!$localKey) {
1221
            $inflector = Inflector::get();
1222
            $localKey = strtolower($inflector->underscore($model::modelName())).'_id';
1223
        }
1224
1225
        return new BelongsToMany($model, $foreignKey, $localKey, $this);
1226
    }
1227
1228
    /////////////////////////////
1229
    // Events
1230
    /////////////////////////////
1231
1232
    /**
1233
     * Gets the event dispatcher.
1234
     *
1235
     * @return EventDispatcher
1236
     */
1237
    public static function getDispatcher($ignoreCache = false)
1238
    {
1239
        $class = get_called_class();
1240
        if ($ignoreCache || !isset(self::$dispatchers[$class])) {
1241
            self::$dispatchers[$class] = new EventDispatcher();
1242
        }
1243
1244
        return self::$dispatchers[$class];
1245
    }
1246
1247
    /**
1248
     * Subscribes to a listener to an event.
1249
     *
1250
     * @param string   $event    event name
1251
     * @param callable $listener
1252
     * @param int      $priority optional priority, higher #s get called first
1253
     */
1254
    public static function listen($event, callable $listener, $priority = 0)
1255
    {
1256
        static::getDispatcher()->addListener($event, $listener, $priority);
1257
    }
1258
1259
    /**
1260
     * Adds a listener to the model.creating and model.updating events.
1261
     *
1262
     * @param callable $listener
1263
     * @param int      $priority
1264
     */
1265
    public static function saving(callable $listener, $priority = 0)
1266
    {
1267
        static::listen(ModelEvent::CREATING, $listener, $priority);
1268
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1269
    }
1270
1271
    /**
1272
     * Adds a listener to the model.created and model.updated events.
1273
     *
1274
     * @param callable $listener
1275
     * @param int      $priority
1276
     */
1277
    public static function saved(callable $listener, $priority = 0)
1278
    {
1279
        static::listen(ModelEvent::CREATED, $listener, $priority);
1280
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1281
    }
1282
1283
    /**
1284
     * Adds a listener to the model.creating event.
1285
     *
1286
     * @param callable $listener
1287
     * @param int      $priority
1288
     */
1289
    public static function creating(callable $listener, $priority = 0)
1290
    {
1291
        static::listen(ModelEvent::CREATING, $listener, $priority);
1292
    }
1293
1294
    /**
1295
     * Adds a listener to the model.created event.
1296
     *
1297
     * @param callable $listener
1298
     * @param int      $priority
1299
     */
1300
    public static function created(callable $listener, $priority = 0)
1301
    {
1302
        static::listen(ModelEvent::CREATED, $listener, $priority);
1303
    }
1304
1305
    /**
1306
     * Adds a listener to the model.updating event.
1307
     *
1308
     * @param callable $listener
1309
     * @param int      $priority
1310
     */
1311
    public static function updating(callable $listener, $priority = 0)
1312
    {
1313
        static::listen(ModelEvent::UPDATING, $listener, $priority);
1314
    }
1315
1316
    /**
1317
     * Adds a listener to the model.updated event.
1318
     *
1319
     * @param callable $listener
1320
     * @param int      $priority
1321
     */
1322
    public static function updated(callable $listener, $priority = 0)
1323
    {
1324
        static::listen(ModelEvent::UPDATED, $listener, $priority);
1325
    }
1326
1327
    /**
1328
     * Adds a listener to the model.deleting event.
1329
     *
1330
     * @param callable $listener
1331
     * @param int      $priority
1332
     */
1333
    public static function deleting(callable $listener, $priority = 0)
1334
    {
1335
        static::listen(ModelEvent::DELETING, $listener, $priority);
1336
    }
1337
1338
    /**
1339
     * Adds a listener to the model.deleted event.
1340
     *
1341
     * @param callable $listener
1342
     * @param int      $priority
1343
     */
1344
    public static function deleted(callable $listener, $priority = 0)
1345
    {
1346
        static::listen(ModelEvent::DELETED, $listener, $priority);
1347
    }
1348
1349
    /**
1350
     * Dispatches an event.
1351
     *
1352
     * @param string $eventName
1353
     *
1354
     * @return ModelEvent
1355
     */
1356
    protected function dispatch($eventName)
1357
    {
1358
        $event = new ModelEvent($this);
1359
1360
        return static::getDispatcher()->dispatch($eventName, $event);
1361
    }
1362
1363
    /**
1364
     * Dispatches the given event and checks if it was successful.
1365
     *
1366
     * @param string $eventName
1367
     *
1368
     * @return bool true if the events were successfully propagated
1369
     */
1370
    private function handleDispatch($eventName)
1371
    {
1372
        $event = $this->dispatch($eventName);
1373
1374
        return !$event->isPropagationStopped();
1375
    }
1376
1377
    /////////////////////////////
1378
    // Validation
1379
    /////////////////////////////
1380
1381
    /**
1382
     * Validates and marshals a value to storage.
1383
     *
1384
     * @param array  $property
1385
     * @param string $propertyName
1386
     * @param mixed  $value
1387
     *
1388
     * @return bool
1389
     */
1390
    private function filterAndValidate(array $property, $propertyName, &$value)
1391
    {
1392
        // assume empty string is a null value for properties
1393
        // that are marked as optionally-null
1394
        if ($property['null'] && empty($value)) {
1395
            $value = null;
1396
1397
            return true;
1398
        }
1399
1400
        // validate
1401
        list($valid, $value) = $this->validate($property, $propertyName, $value);
1402
1403
        // unique?
1404
        if ($valid && $property['unique'] && ($this->_id === false || $value != $this->ignoreUnsaved()->$propertyName)) {
1405
            $valid = $this->checkUniqueness($property, $propertyName, $value);
1406
        }
1407
1408
        return $valid;
1409
    }
1410
1411
    /**
1412
     * Validates a value for a property.
1413
     *
1414
     * @param array  $property
1415
     * @param string $propertyName
1416
     * @param mixed  $value
1417
     *
1418
     * @return array
1419
     */
1420
    private function validate(array $property, $propertyName, $value)
1421
    {
1422
        $valid = true;
1423
1424
        if (isset($property['validate']) && is_callable($property['validate'])) {
1425
            $valid = call_user_func_array($property['validate'], [$value]);
1426
        } elseif (isset($property['validate'])) {
1427
            $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...
1428
        }
1429
1430 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...
1431
            $this->app['errors']->push([
1432
                'error' => self::ERROR_VALIDATION_FAILED,
1433
                'params' => [
1434
                    'field' => $propertyName,
1435
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1436
        }
1437
1438
        return [$valid, $value];
1439
    }
1440
1441
    /**
1442
     * Checks if a value is unique for a property.
1443
     *
1444
     * @param array  $property
1445
     * @param string $propertyName
1446
     * @param mixed  $value
1447
     *
1448
     * @return bool
1449
     */
1450
    private function checkUniqueness(array $property, $propertyName, $value)
1451
    {
1452
        $n = static::where([$propertyName => $value])->count();
1453 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...
1454
            $this->app['errors']->push([
1455
                'error' => self::ERROR_NOT_UNIQUE,
1456
                'params' => [
1457
                    'field' => $propertyName,
1458
                    'field_name' => (isset($property['title'])) ? $property['title'] : Inflector::get()->titleize($propertyName), ], ]);
1459
1460
            return false;
1461
        }
1462
1463
        return true;
1464
    }
1465
1466
    /**
1467
     * Gets the marshaled default value for a property (if set).
1468
     *
1469
     * @param string $property
1470
     *
1471
     * @return mixed
1472
     */
1473
    private function getPropertyDefault(array $property)
1474
    {
1475
        return array_value($property, 'default');
1476
    }
1477
}
1478