Completed
Push — master ( e24a08...649c71 )
by Joshua
9s
created

Model::filterNotSavedProperties()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 6
Ratio 60 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 6
loc 10
rs 9.2
cc 4
eloc 6
nc 3
nop 1
1
<?php
2
3
namespace As3\Modlr\Models;
4
5
use As3\Modlr\Models\Relationships;
6
use As3\Modlr\Store\Store;
7
use As3\Modlr\Metadata\EntityMetadata;
8
9
/**
10
 * Represents a data record from a persistence (database) layer.
11
 *
12
 * @author Jacob Bare <[email protected]>
13
 */
14
class Model extends AbstractModel
15
{
16
    /**
17
     * Enables/disables collection auto-initialization on iteration.
18
     * Will not load/fill the collection from the database if false.
19
     * Is useful for large hasMany iterations where only id and type are required (ala serialization).
20
     *
21
     * @var bool
22
     */
23
    protected $collectionAutoInit = true;
24
25
    /**
26
     * The Model's has-one relationships
27
     *
28
     * @var Relationships\HasOne
29
     */
30
    protected $hasOneRelationships;
31
32
    /**
33
     * The Model's has-many relationships
34
     *
35
     * @var Relationships\HasMany
36
     */
37
    protected $hasManyRelationships;
38
39
    /**
40
     * The id value of this model.
41
     * Always converted to a string when in the model context.
42
     *
43
     * @var string
44
     */
45
    protected $identifier;
46
47
48
    /**
49
     * The metadata that defines this Model.
50
     *
51
     * @var EntityMetadata
52
     */
53
    protected $metadata;
54
55
    /**
56
     * Constructor.
57
     *
58
     * @param   EntityMetadata  $metadata       The internal entity metadata that supports this Model.
59
     * @param   string          $identifier     The database identifier.
60
     * @param   Store           $store          The model store service for handling persistence operations.
61
     * @param   array|null      $properties     The model's properties from the db layer to init the model with. New models will constructed with a null record.
62
     */
63
    public function __construct(EntityMetadata $metadata, $identifier, Store $store, array $properties = null)
64
    {
65
        $this->identifier = $identifier;
66
        parent::__construct($metadata, $store, $properties);
67
    }
68
69
    /**
70
     * Cloner.
71
     * Ensures sub objects are also cloned.
72
     *
73
     */
74
    public function __clone()
75
    {
76
        parent::__clone();
77
        $this->hasOneRelationships = clone $this->hasOneRelationships;
78
        $this->hasManyRelationships = clone $this->hasManyRelationships;
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     *
84
     * Overloaded to support relationships.
85
     *
86
     */
87
    public function apply(array $properties)
88
    {
89
        foreach ($properties as $key => $value) {
90
            if (true === $this->isHasOne($key)) {
91
                if (empty($value)) {
92
                    $this->clear($key);
93
                    continue;
94
                }
95
                $value = $this->store->loadProxyModel($value['type'], $value['id']);
96
                $this->set($key, $value);
97
                continue;
98
            }
99
100
        }
101
102
        foreach ($this->getMetadata()->getRelationships() as $key => $relMeta) {
103
            if (true === $relMeta->isOne()) {
104
                continue;
105
            }
106
            if (!isset($properties[$key]) || true === $relMeta->isInverse) {
107
                continue;
108
            }
109
            $this->clear($key);
110
            $collection = $this->store->createCollection($relMeta, $properties[$key]);
111
            foreach ($collection->allWithoutLoad() as $value) {
112
                $this->push($key, $value);
0 ignored issues
show
Compatibility introduced by
$value of type object<As3\Modlr\Models\AbstractModel> is not a sub-type of object<As3\Modlr\Models\Model>. It seems like you assume a child class of the class As3\Modlr\Models\AbstractModel to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
113
            }
114
        }
115
        return parent::apply($properties);
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     *
121
     * Overloaded to support relationships.
122
     */
123 View Code Duplication
    public function clear($key)
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...
124
    {
125
        if (true === $this->isHasOne($key)) {
126
            return $this->setHasOne($key, null);
127
        }
128
        if (true === $this->isInverse($key)) {
129
            throw ModelException::cannotModifyInverse($this, $key);
130
        }
131
        if (true === $this->isHasMany($key)) {
132
            $collection = $this->hasManyRelationships->get($key);
133
            $collection->clear();
134
            $this->doDirtyCheck();
135
            return $this;
136
        }
137
        return parent::clear($key);
138
    }
139
140
    /**
141
     * Enables or disables has-many collection auto-initialization from the database.
142
     *
143
     * @param   bool    $bit    Whether to enable/disable.
144
     * @return  self
145
     */
146
    public function enableCollectionAutoInit($bit = true)
147
    {
148
        $this->collectionAutoInit = (Boolean) $bit;
149
        return $this;
150
    }
151
152
    /**
153
     * Marks the record for deletion.
154
     * Will not remove from the database until $this->save() is called.
155
     *
156
     * @api
157
     * @return  self
158
     * @throws  \RuntimeException   If a new (unsaved) model is deleted.
159
     */
160
    public function delete()
161
    {
162
        if (true === $this->getState()->is('new')) {
163
            throw new \RuntimeException('You cannot delete a new model');
164
        }
165
        if (true === $this->getState()->is('deleted')) {
166
            return $this;
167
        }
168
        $this->getState()->setDeleting();
169
        return $this;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     *
175
     * Overloaded to support relationships.
176
     *
177
     */
178
    public function get($key)
179
    {
180
        if (true === $this->isRelationship($key)) {
181
            return $this->getRelationship($key);
182
        }
183
        return parent::get($key);
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     *
189
     * Overloaded to support relationships.
190
     *
191
     */
192
    public function getChangeSet()
193
    {
194
        $changeset = parent::getChangeSet();
195
        $changeset['hasOne']  = $this->filterNotSavedProperties($this->hasOneRelationships->calculateChangeSet());
196
        $changeset['hasMany'] = $this->filterNotSavedProperties($this->hasManyRelationships->calculateChangeSet());
197
        return $changeset;
198
    }
199
200
    /**
201
     * Gets the composite key of the model by combining the model type with the unique id.
202
     *
203
     * @api
204
     * @return  string
205
     */
206
    public function getCompositeKey()
207
    {
208
        return sprintf('%s.%s', $this->getType(), $this->getId());
209
    }
210
211
    /**
212
     * Gets the unique identifier of this model.
213
     *
214
     * @api
215
     * @return  string
216
     */
217
    public function getId()
218
    {
219
        return $this->identifier;
220
    }
221
222
    /**
223
     * Gets the metadata for this model.
224
     *
225
     * @api
226
     * @return  EntityMetadata
227
     */
228
    public function getMetadata()
229
    {
230
        return $this->metadata;
231
    }
232
233
    /**
234
     * Gets the model type.
235
     *
236
     * @api
237
     * @return  string
238
     */
239
    public function getType()
240
    {
241
        return $this->metadata->type;
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     *
247
     * Overloaded to support relationships.
248
     *
249
     */
250
    public function initialize(array $properties = null)
251
    {
252
        $hasOne = [];
253
        $hasMany = [];
254
255
        if (null !== $properties) {
256
            foreach ($properties as $key => $value) {
257
                if (true === $this->isHasOne($key)) {
258
                    // Load hasOne relationship.
259
                    $hasOne[$key] = $this->getStore()->loadProxyModel($value['type'], $value['id']);
260
                    continue;
261
                }
262
            }
263
        }
264
265
        foreach ($this->getMetadata()->getRelationships() as $key => $relMeta) {
266
            if (true === $relMeta->isOne()) {
267
                continue;
268
            }
269
            if (true === $relMeta->isInverse) {
270
                $hasMany[$key] = $this->getStore()->createInverseCollection($relMeta, $this);
271
            } else {
272
                $references = !isset($properties[$key]) ? [] : $properties[$key];
273
                $hasMany[$key] = $this->getStore()->createCollection($relMeta, $references);
274
            }
275
        }
276
277
        $this->hasOneRelationships  = (null === $this->hasOneRelationships) ? new Relationships\HasOne($hasOne) : $this->hasOneRelationships->replace($hasOne);
278
        $this->hasManyRelationships = (null === $this->hasManyRelationships) ? new Relationships\HasMany($hasMany) : $this->hasManyRelationships->replace($hasMany);
279
        return parent::initialize($properties);
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     *
285
     * Overloaded to support relationships.
286
     *
287
     */
288
    public function isDirty()
289
    {
290
        return true === parent::isDirty()
291
            || true === $this->hasOneRelationships->areDirty()
292
            || true === $this->hasManyRelationships->areDirty()
293
        ;
294
    }
295
296
    /**
297
     * Determines if a property key is a has-many relationship.
298
     *
299
     * @api
300
     * @param   string  $key    The property key.
301
     * @return  bool
302
     */
303
    public function isHasMany($key)
304
    {
305
        if (false === $this->isRelationship($key)) {
306
            return false;
307
        }
308
        return $this->getMetadata()->getRelationship($key)->isMany();
309
    }
310
311
    /**
312
     * Determines if a property key is a has-one relationship.
313
     *
314
     * @api
315
     * @param   string  $key    The property key.
316
     * @return  bool
317
     */
318
    public function isHasOne($key)
319
    {
320
        if (false === $this->isRelationship($key)) {
321
            return false;
322
        }
323
        return $this->getMetadata()->getRelationship($key)->isOne();
324
    }
325
326
    /**
327
     * Determines if a property key is a an inverse relationship.
328
     *
329
     * @api
330
     * @param   string  $key    The property key.
331
     * @return  bool
332
     */
333
    public function isInverse($key)
334
    {
335
        if (false === $this->isRelationship($key)) {
336
            return false;
337
        }
338
        return $this->getMetadata()->getRelationship($key)->isInverse;
339
    }
340
341
    /**
342
     * Determines if a property key is a relationship (either has-one or has-many).
343
     *
344
     * @api
345
     * @param   string  $key    The property key.
346
     * @return  bool
347
     */
348
    public function isRelationship($key)
349
    {
350
        return $this->getMetadata()->hasRelationship($key);
351
    }
352
353
    /**
354
     * Pushes a Model into a has-many relationship collection.
355
     * This method must be used for has-many relationships. Direct set is not supported.
356
     * To completely replace a has-many, call clear() first and then push() the new Models.
357
     *
358
     * @api
359
     * @param   string  $key
360
     * @param   Model   $model
361
     * @return  self
362
     */
363 View Code Duplication
    public function push($key, Model $model)
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...
364
    {
365
        if (true === $this->isHasOne($key)) {
366
            return $this->setHasOne($key, $model);
367
        }
368
        if (false === $this->isHasMany($key)) {
369
            return $this;
370
        }
371
        if (true === $this->isInverse($key)) {
372
            throw ModelException::cannotModifyInverse($this, $key);
373
        }
374
        $this->touch();
375
        $collection = $this->hasManyRelationships->get($key);
376
        $collection->push($model);
377
        $this->doDirtyCheck();
378
        return $this;
379
    }
380
381
    /**
382
     * Reloads the model from the database.
383
     *
384
     * @api
385
     * @return  self
386
     */
387
    public function reload()
388
    {
389
        return $this->touch(true);
390
    }
391
392
    /**
393
     * Removes a specific Model from a has-many relationship collection.
394
     *
395
     * @api
396
     * @param   string  $key    The has-many relationship key.
397
     * @param   Model   $model  The model to remove from the collection.
398
     * @return  self
399
     */
400 View Code Duplication
    public function remove($key, Model $model)
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...
401
    {
402
        if (false === $this->isHasMany($key)) {
403
            return $this;
404
        }
405
        if (true === $this->isInverse($key)) {
406
            throw ModelException::cannotModifyInverse($this, $key);
407
        }
408
        $this->touch();
409
        $collection = $this->hasManyRelationships->get($key);
410
        $collection->remove($model);
411
        $this->doDirtyCheck();
412
        return $this;
413
    }
414
415
    /**
416
     * {@inheritdoc}
417
     * Overloaded to support relationship rollback.
418
     */
419
    public function rollback()
420
    {
421
        $this->hasOneRelationships->rollback();
422
        $this->hasManyRelationships->rollback();
423
        return parent::rollback();
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     *
429
     * Overloaded to support relationships.
430
     * Sets a model property: an attribute value, a has-one model, or an entire has-many model collection.
431
     * Note: To push/remove a single Model into a has-many collection, or clear a collection, use @see push(), remove() and clear().
432
     *
433
     */
434
    public function set($key, $value)
435
    {
436
        if (true === $this->isRelationship($key)) {
437
            return $this->setRelationship($key, $value);
438
        }
439
        return parent::set($key, $value);
440
    }
441
442
    /**
443
     * Saves the model.
444
     *
445
     * @api
446
     * @param   Implement cascade relationship saves. Or should the store handle this?
447
     * @return  self
448
     */
449
    public function save()
450
    {
451
        if (true === $this->getState()->is('deleted')) {
452
            return $this;
453
        }
454
        $this->store->commit($this);
455
        return $this;
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     *
461
     * Overloaded to support relationships.
462
     */
463
    protected function filterNotSavedProperties(array $properties)
464
    {
465 View Code Duplication
        foreach ($this->getMetadata()->getRelationships() as $fieldKey => $propMeta) {
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...
466
            if (true === $propMeta->shouldSave() || !isset($properties[$fieldKey])) {
467
                continue;
468
            }
469
            unset($properties[$fieldKey]);
470
        }
471
        return parent::filterNotSavedProperties($properties);
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     *
477
     * Overloaded to support global model defaults.
478
     *
479
     */
480
    protected function applyDefaultAttrValues(array $attributes = [])
481
    {
482
        $attributes = parent::applyDefaultAttrValues($attributes);
483
484
        // Set defaults for the entire entity.
485
        foreach ($this->getMetadata()->defaultValues as $key => $value) {
486
            if (isset($attributes[$key])) {
487
                continue;
488
            }
489
            $attributes[$key] = $this->convertAttributeValue($key, $value);
490
        }
491
        return $attributes;
492
    }
493
494
    /**
495
     * Gets a relationship value.
496
     *
497
     * @param   string  $key    The relationship key (field) name.
498
     * @return  Model|array|null
499
     * @throws  \RuntimeException If hasMany relationships are accessed directly.
500
     */
501
    protected function getRelationship($key)
502
    {
503
        if (true === $this->isHasOne($key)) {
504
            $this->touch();
505
            return $this->hasOneRelationships->get($key);
506
        }
507
        if (true === $this->isHasMany($key)) {
508
            $this->touch();
509
            $collection = $this->hasManyRelationships->get($key);
510
            if ($collection->isLoaded($collection)) {
511
                return iterator_to_array($collection);
512
            }
513
            return (true === $this->collectionAutoInit) ? iterator_to_array($collection) : $collection->allWithoutLoad();
514
        }
515
        return null;
516
    }
517
518
    /**
519
     * Sets a has-one relationship.
520
     *
521
     * @param   string      $key    The relationship key (field) name.
522
     * @param   Model|null  $model  The model to relate.
523
     * @return  self
524
     */
525 View Code Duplication
    protected function setHasOne($key, Model $model = null)
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...
526
    {
527
        if (true === $this->isInverse($key)) {
528
            throw ModelException::cannotModifyInverse($this, $key);
529
        }
530
        if (null !== $model) {
531
            $this->validateRelSet($key, $model->getType());
532
        }
533
        $this->touch();
534
        $this->hasOneRelationships->set($key, $model);
535
        $this->doDirtyCheck();
536
        return $this;
537
    }
538
539
    /**
540
     * Sets a relationship value.
541
     *
542
     * @param   string      $key
543
     * @param   Model|null  $value
544
     * @return  self
545
     */
546
    protected function setRelationship($key, $value)
547
    {
548
        if (true === $this->isHasOne($key)) {
549
            return $this->setHasOne($key, $value);
550
        }
551
        if (true === $this->isHasMany($key)) {
552
            throw new \RuntimeException('You cannot set a hasMany relationship directly. Please access using push(), clear(), and/or remove()');
553
        }
554
        return $this;
555
    }
556
557
    /**
558
     * {@inheritdoc}
559
     *
560
     * Overloaded to handle loading from the database.
561
     * If the model is currently empty, it will query the database and fill/load the model.
562
     *
563
     */
564
    protected function touch($force = false)
565
    {
566
        if (true === $this->getState()->is('deleted')) {
567
            return $this;
568
        }
569
        if (true === $this->getState()->is('empty') || true === $force) {
570
            $record = $this->store->retrieveRecord($this->getType(), $this->getId());
571
            $this->initialize($record->getProperties());
572
            $this->getState()->setLoaded();
573
        }
574
        return $this;
575
    }
576
577
    /**
578
     * Validates that the model type (from a Model or Collection instance) can be set to the relationship field.
579
     *
580
     * @param   string  $relKey The relationship field key.
581
     * @param   string  $type   The model type that is being related.
582
     * @return  self
583
     */
584
    protected function validateRelSet($relKey, $type)
585
    {
586
        $relMeta = $this->getMetadata()->getRelationship($relKey);
587
        $relatedModelMeta = $this->getStore()->getMetadataForRelationship($relMeta);
0 ignored issues
show
Bug introduced by
It seems like $relMeta defined by $this->getMetadata()->getRelationship($relKey) on line 586 can be null; however, As3\Modlr\Store\Store::g...tadataForRelationship() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
588
        $this->getStore()->validateRelationshipSet($relatedModelMeta, $type);
589
        return $this;
590
    }
591
}
592