Model::touch()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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