Completed
Push — master ( 9e9fff...f69d52 )
by Jacob
8s
created

AbstractModel::initialize()   D

Complexity

Conditions 12
Paths 160

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 40
rs 4.8484
cc 12
eloc 23
nc 160
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace As3\Modlr\Models;
4
5
use As3\Modlr\Metadata\Interfaces\AttributeInterface;
6
use As3\Modlr\Store\Store;
7
8
/**
9
 * Represents a record from a persistence (database) layer.
10
 * Can either be a root record, or an embedded fragment of a root record.
11
 *
12
 * @author Jacob Bare <[email protected]>
13
 */
14
abstract class AbstractModel
15
{
16
    /**
17
     * The model's attributes
18
     *
19
     * @var Attributes
20
     */
21
    protected $attributes;
22
23
    /**
24
     * The Model's has-one embeds
25
     *
26
     * @var Embeds\HasOne
27
     */
28
    protected $hasOneEmbeds;
29
30
    /**
31
     * The Model's has-many embeds
32
     *
33
     * @var Embeds\HasMany
34
     */
35
    protected $hasManyEmbeds;
36
37
    /**
38
     * The metadata that defines this Model.
39
     *
40
     * @var AttributeInterface
41
     */
42
    protected $metadata;
43
44
    /**
45
     * The model state.
46
     *
47
     * @var State
48
     */
49
    protected $state;
50
51
    /**
52
     * The Model Store for handling lifecycle operations.
53
     *
54
     * @var Store
55
     */
56
    protected $store;
57
58
    /**
59
     * Constructor.
60
     *
61
     * @param   AttributeInterface  $metadata
62
     * @param   Store               $store
63
     * @param   array|null          $properties
64
     */
65
    public function __construct(AttributeInterface $metadata, Store $store, array $properties = null)
66
    {
67
        $this->state = new State();
68
        $this->metadata = $metadata;
69
        $this->store = $store;
70
        $this->initialize($properties);
71
    }
72
73
    /**
74
     * Cloner.
75
     * Ensures sub objects are also cloned.
76
     *
77
     */
78
    public function __clone()
79
    {
80
        $this->attributes = clone $this->attributes;
81
        $this->hasOneEmbeds = clone $this->hasOneEmbeds;
82
        $this->hasManyEmbeds = clone $this->hasManyEmbeds;
83
        $this->state = clone $this->state;
84
    }
85
86
    /**
87
     * Applies an array of raw model properties to the model instance.
88
     *
89
     * @todo    Confirm that we want this method. It's currently used for creating and updating via the API adapter. Also see initialize()
90
     * @param   array   $properties     The properties to apply.
91
     * @return  self
92
     */
93
    public function apply(array $properties)
94
    {
95
        $properties = $this->applyDefaultAttrValues($properties);
96
        foreach ($properties as $key => $value) {
97
            if (true === $this->isAttribute($key)) {
98
                $this->set($key, $value);
99
                continue;
100
            }
101
102
            if (true === $this->isEmbedHasOne($key)) {
103
                if (empty($value)) {
104
                    $this->clear($key);
105
                    continue;
106
                }
107
                $embed = $this->get($key) ?: $this->createEmbedFor($key, $value);
0 ignored issues
show
Unused Code introduced by
The call to AbstractModel::createEmbedFor() has too many arguments starting with $value.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
108
                $embed->apply($value);
109
                $this->set($key, $embed);
110
                continue;
111
            }
112
        }
113
114
        foreach ($this->getMetadata()->getEmbeds() as $key => $embeddedPropMeta) {
115
            if (true === $embeddedPropMeta->isOne() || !isset($properties[$key])) {
116
                continue;
117
            }
118
119
            // @todo This will always mark the model as dirty, even if the applied embed values are the same as the original.
120
            $this->clear($key);
121
            $collection = $this->getStore()->createEmbedCollection($embeddedPropMeta, $properties[$key]);
122
            foreach ($collection as $value) {
123
                $this->pushEmbed($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\Embed>. 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...
124
            }
125
        }
126
127
        $this->doDirtyCheck();
128
        return $this;
129
    }
130
131
    /**
132
     * Clears a property value.
133
     * For an attribute, will set the value to null.
134
     * For collections, will clear the collection contents.
135
     *
136
     * @api
137
     * @param   string  $key    The property key.
138
     * @return  self
139
     */
140
    public function clear($key)
141
    {
142
        if (true === $this->isAttribute($key)) {
143
            return $this->setAttribute($key, null);
144
        }
145
        if (true === $this->isEmbedHasOne($key)) {
146
            return $this->setEmbedHasOne($key, null);
147
        }
148
        if (true === $this->isEmbedHasMany($key)) {
149
            $collection = $this->hasManyEmbeds->get($key);
150
            $collection->clear();
151
            $this->doDirtyCheck();
152
            return $this;
153
        }
154
        return $this;
155
    }
156
157
    /**
158
     * Creates a new Embed model instance for the provided property key.
159
     *
160
     * @param   string  $key
161
     * @return  Embed
162
     * @throws  \RuntimeException
163
     */
164
    public function createEmbedFor($key)
165
    {
166
        if (false === $this->isEmbed($key)) {
167
            throw new \RuntimeException(sprintf('Unable to create an Embed instance for property key "%s" - the property is not an embed.', $key));
168
        }
169
170
        $embedMeta = $this->getMetadata()->getEmbed($key)->embedMeta;
171
        $embed = $this->getStore()->loadEmbed($embedMeta, []);
172
        $embed->getState()->setNew();
173
        return $embed;
174
    }
175
176
    /**
177
     * Gets a model property.
178
     * Returns null if the property does not exist on the model or is not set.
179
     *
180
     * @api
181
     * @param   string  $key    The property field key.
182
     * @return  Model|Model[]|Embed|Collections\EmbedCollection|null|mixed
183
     */
184
    public function get($key)
185
    {
186
        if (true === $this->isAttribute($key)) {
187
            return $this->getAttribute($key);
188
        }
189
        if (true === $this->isEmbed($key)) {
190
            return $this->getEmbed($key);
191
        }
192
    }
193
194
    /**
195
     * Gets the current change set of properties.
196
     *
197
     * @api
198
     * @return  array
199
     */
200
    public function getChangeSet()
201
    {
202
        return [
203
            'attributes'    => $this->filterNotSavedProperties($this->attributes->calculateChangeSet()),
204
            'embedOne'      => $this->hasOneEmbeds->calculateChangeSet(),
205
            'embedMany'     => $this->hasManyEmbeds->calculateChangeSet(),
206
        ];
207
    }
208
209
    /**
210
     * Gets the metadata for this model.
211
     *
212
     * @api
213
     * @return  AttributeInterface
214
     */
215
    public function getMetadata()
216
    {
217
        return $this->metadata;
218
    }
219
220
    /**
221
     * Gets the model state object.
222
     *
223
     * @todo    Should this be public? State setting should likely be locked from the outside world.
224
     * @return  State
225
     */
226
    public function getState()
227
    {
228
        return $this->state;
229
    }
230
231
    /**
232
     * Gets the model store.
233
     *
234
     * @api
235
     * @return  Store
236
     */
237
    public function getStore()
238
    {
239
        return $this->store;
240
    }
241
242
    /**
243
     * Initializes the model and loads its attributes and relationships.
244
     *
245
     * @todo    Made public so collections can initialize models. Not sure if we want this??
246
     * @param   array|null      $properties     The db properties to apply.
247
     * @return  self
248
     */
249
    public function initialize(array $properties = null)
250
    {
251
        $attributes = [];
252
        $embedOne = [];
253
        $embedMany = [];
254
255
        if (null !== $properties) {
256
            $attributes = $this->applyDefaultAttrValues($attributes);
257
            foreach ($properties as $key => $value) {
258
                if (true === $this->isAttribute($key)) {
259
                    // Load attribute.
260
                    $attributes[$key] = $this->convertAttributeValue($key, $value);
261
                } else if (true === $this->isEmbedHasOne($key)) {
262
                    // Load embed one.
263
                    $embedOne[$key] = $this->getStore()->loadEmbed($this->getMetadata()->getEmbed($key)->embedMeta, $value);
264
                }
265
            }
266
        }
267
268
        foreach ($this->getMetadata()->getEmbeds() as $key => $embeddedPropMeta) {
269
            // Always load embedded collections, regardless if data is set.
270
            if (true === $embeddedPropMeta->isOne()) {
271
                continue;
272
            }
273
            $embeds = !isset($properties[$key]) ? [] : $properties[$key];
274
            $embedMany[$key] = $this->getStore()->createEmbedCollection($embeddedPropMeta, $embeds);
275
        }
276
277
        $this->attributes    = (null === $this->attributes) ? new Attributes($attributes) : $this->attributes->replace($attributes);
278
        $this->hasOneEmbeds  = (null === $this->hasOneEmbeds) ? new Embeds\HasOne($embedOne) : $this->hasOneEmbeds->replace($embedOne);
279
        $this->hasManyEmbeds = (null === $this->hasManyEmbeds) ? new Embeds\HasMany($embedMany) : $this->hasManyEmbeds->replace($embedMany);
280
281
        if (true === $this->getState()->is('new')) {
282
            // Ensure default values are applied to new models.
283
            $this->apply([]);
284
        }
285
286
        $this->doDirtyCheck();
287
        return $this;
288
    }
289
290
    /**
291
     * Determines if a property key is an attribute.
292
     *
293
     * @api
294
     * @param   string  $key    The property key.
295
     * @return  bool
296
     */
297
    public function isAttribute($key)
298
    {
299
        return $this->getMetadata()->hasAttribute($key);
300
    }
301
302
    /**
303
     * Determines if the model is currently dirty.
304
     *
305
     * @api
306
     * @return  bool
307
     */
308
    public function isDirty()
309
    {
310
        return true === $this->attributes->areDirty()
311
            || true === $this->hasOneEmbeds->areDirty()
312
            || true === $this->hasManyEmbeds->areDirty()
313
        ;
314
    }
315
316
    /**
317
     * Determines if a property key is an embedded property.
318
     *
319
     * @api
320
     * @param   string  $key    The property key.
321
     * @return  bool
322
     */
323
    public function isEmbed($key)
324
    {
325
        return $this->getMetadata()->hasEmbed($key);
326
    }
327
328
    /**
329
     * Determines if a property key is a has-many embed.
330
     *
331
     * @api
332
     * @param   string  $key    The property key.
333
     * @return  bool
334
     */
335
    public function isEmbedHasMany($key)
336
    {
337
        if (false === $this->isEmbed($key)) {
338
            return false;
339
        }
340
        return $this->getMetadata()->getEmbed($key)->isMany();
341
    }
342
343
    /**
344
     * Determines if a property key is a has-one embed.
345
     *
346
     * @api
347
     * @param   string  $key    The property key.
348
     * @return  bool
349
     */
350
    public function isEmbedHasOne($key)
351
    {
352
        if (false === $this->isEmbed($key)) {
353
            return false;
354
        }
355
        return $this->getMetadata()->getEmbed($key)->isOne();
356
    }
357
358
    /**
359
     * Pushes an Embed into a has-many embed collection.
360
     * This method must be used for has-many embeds. Direct set is not supported.
361
     * To completely replace call clear() first and then pushEmbed() the new Embeds.
362
     *
363
     * @api
364
     * @param   string  $key
365
     * @param   Embed   $embed
366
     * @return  self
367
     */
368
    public function pushEmbed($key, Embed $embed)
369
    {
370
        if (true === $this->isEmbedHasOne($key)) {
371
            return $this->setEmbedHasOne($key, $embed);
372
        }
373
        if (false === $this->isEmbedHasMany($key)) {
374
            return $this;
375
        }
376
        $this->touch();
377
        $collection = $this->hasManyEmbeds->get($key);
378
        $collection->push($embed);
379
        $this->doDirtyCheck();
380
        return $this;
381
    }
382
383
    /**
384
     * Removes a specific Embed from a has-many embed collection.
385
     *
386
     * @api
387
     * @param   string  $key    The has-many embed key.
388
     * @param   Embed   $embed  The embed to remove from the collection.
389
     * @return  self
390
     */
391 View Code Duplication
    public function removeEmbed($key, Embed $embed)
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...
392
    {
393
        if (false === $this->isEmbedHasMany($key)) {
394
            return $this;
395
        }
396
        $this->touch();
397
        $collection = $this->hasManyEmbeds->get($key);
398
        $collection->remove($embed);
399
        $this->doDirtyCheck();
400
        return $this;
401
    }
402
403
    /**
404
     * Rolls back a model to its original values.
405
     *
406
     * @api
407
     * @return  self
408
     */
409
    public function rollback()
410
    {
411
        $this->attributes->rollback();
412
        $this->hasOneEmbeds->rollback();
413
        $this->hasManyEmbeds->rollback();
414
        $this->doDirtyCheck();
415
        return $this;
416
    }
417
418
    /**
419
     * Sets a model property.
420
     *
421
     * @api
422
     * @param   string  $key            The property field key.
423
     * @param   Model|Embed|null|mixed  The value to set.
424
     * @return  self.
0 ignored issues
show
Documentation introduced by
The doc-type self. could not be parsed: Unknown type name "self." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
425
     */
426
    public function set($key, $value)
427
    {
428
        if (true === $this->isAttribute($key)) {
429
            return $this->setAttribute($key, $value);
430
        }
431
        if (true === $this->isEmbed($key)) {
432
            return $this->setEmbed($key, $value);
433
        }
434
    }
435
436
    /**
437
     * Determines if the model uses a particlar mixin.
438
     *
439
     * @api
440
     * @param   string  $name
441
     * @return  bool
442
     */
443
    public function usesMixin($name)
444
    {
445
        return $this->getMetadata()->hasMixin($name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface As3\Modlr\Metadata\Interfaces\AttributeInterface as the method hasMixin() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EmbedMetadata, As3\Modlr\Metadata\EntityMetadata.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
446
    }
447
448
    /**
449
     * Applies default attribute values from metadata, if set.
450
     *
451
     * @param   array   $attributes     The attributes to apply the defaults to.
452
     * @return  array
453
     */
454
    protected function applyDefaultAttrValues(array $attributes = [])
455
    {
456
        // Set defaults for each attribute.
457
        foreach ($this->getMetadata()->getAttributes() as $key => $attrMeta) {
458
            if (!isset($attrMeta->defaultValue) || isset($attributes[$key])) {
459
                continue;
460
            }
461
            $attributes[$key] = $this->convertAttributeValue($key, $attrMeta->defaultValue);
462
        }
463
        return $attributes;
464
    }
465
466
    /**
467
     * Converts an attribute value to the appropriate data type.
468
     *
469
     * @param   string  $key
470
     * @param   mixed   $value
471
     * @return  mixed
472
     */
473
    protected function convertAttributeValue($key, $value)
474
    {
475
        return $this->store->convertAttributeValue($this->getDataType($key), $value);
476
    }
477
478
    /**
479
     * Does a dirty check and sets the state to this model.
480
     *
481
     * @return  self
482
     */
483
    protected function doDirtyCheck()
484
    {
485
        $this->getState()->setDirty($this->isDirty());
486
        return $this;
487
    }
488
489
    /**
490
     * Removes properties marked as non-saved.
491
     *
492
     * @param   array   $properties
493
     * @return  array
494
     */
495
    protected function filterNotSavedProperties(array $properties)
496
    {
497 View Code Duplication
        foreach ($this->getMetadata()->getAttributes() 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...
498
            if (true === $propMeta->shouldSave() || !isset($properties[$fieldKey])) {
499
                continue;
500
            }
501
            unset($properties[$fieldKey]);
502
        }
503
        return $properties;
504
    }
505
506
    /**
507
     * Gets an attribute value.
508
     *
509
     * @param   string  $key    The attribute key (field) name.
510
     * @return  mixed
511
     */
512
    protected function getAttribute($key)
513
    {
514
        if (true === $this->isCalculatedAttribute($key)) {
515
            return $this->getCalculatedAttribute($key);
516
        }
517
        $this->touch();
518
        return $this->attributes->get($key);
519
    }
520
521
    /**
522
     * Gets a calculated attribute value.
523
     *
524
     * @param   string  $key    The attribute key (field) name.
525
     * @return  mixed
526
     */
527
    protected function getCalculatedAttribute($key)
528
    {
529
        $attrMeta = $this->getMetadata()->getAttribute($key);
530
        $class  = $attrMeta->calculated['class'];
531
        $method = $attrMeta->calculated['method'];
532
533
        $value = $class::$method($this);
534
        return $this->convertAttributeValue($key, $value);
535
    }
536
537
    /**
538
     * Gets a data type from an attribute key.
539
     *
540
     * @param   string  $key The attribute key.
541
     * @return  string
542
     */
543
    protected function getDataType($key)
544
    {
545
        return $this->getMetadata()->getAttribute($key)->dataType;
546
    }
547
548
    /**
549
     * Gets an embed value.
550
     *
551
     * @param   string  $key    The embed key (field) name.
552
     * @return  Embed|Collections\EmbedCollection|null
553
     */
554
    protected function getEmbed($key)
555
    {
556
        if (true === $this->isEmbedHasOne($key)) {
557
            $this->touch();
558
            return $this->hasOneEmbeds->get($key);
559
        }
560
        if (true === $this->isEmbedHasMany($key)) {
561
            $this->touch();
562
            return $this->hasManyEmbeds->get($key);
563
        }
564
        return null;
565
    }
566
567
    /**
568
     * Determines if an attribute key is calculated.
569
     *
570
     * @param   string  $key    The attribute key.
571
     * @return  bool
572
     */
573
    protected function isCalculatedAttribute($key)
574
    {
575
        if (false === $this->isAttribute($key)) {
576
            return false;
577
        }
578
        return $this->getMetadata()->getAttribute($key)->isCalculated();
579
    }
580
581
    /**
582
     * Sets an attribute value.
583
     * Will convert the value to the proper, internal PHP/Modlr data type.
584
     * Will do a dirty check immediately after setting.
585
     *
586
     * @param   string  $key    The attribute key (field) name.
587
     * @param   mixed   $value  The value to apply.
588
     * @return  self
589
     */
590
    protected function setAttribute($key, $value)
591
    {
592
        if (true === $this->isCalculatedAttribute($key)) {
593
            return $this;
594
        }
595
        $this->touch();
596
        $value = $this->convertAttributeValue($key, $value);
597
        $this->attributes->set($key, $value);
598
        $this->doDirtyCheck();
599
        return $this;
600
    }
601
602
    /**
603
     * Sets an embed value.
604
     *
605
     * @param   string      $key
606
     * @param   Embed|null  $value
607
     * @return  self
608
     */
609
    protected function setEmbed($key, $value)
610
    {
611
        if (true === $this->isEmbedHasOne($key)) {
612
            return $this->setEmbedHasOne($key, $value);
613
        }
614
        if (true === $this->isEmbedHasMany($key)) {
615
            throw new \RuntimeException('You cannot set a hasMany embed directly. Please access using pushEmbed(), clear(), and/or remove()');
616
        }
617
        return $this;
618
    }
619
620
    /**
621
     * Sets a has-one embed.
622
     *
623
     * @param   string      $key    The embed key (field) name.
624
     * @param   Embed|null  $embed  The embed to relate.
625
     * @return  self
626
     */
627 View Code Duplication
    protected function setEmbedHasOne($key, Embed $embed = 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...
628
    {
629
        if (null !== $embed) {
630
            $this->validateEmbedSet($key, $embed->getName());
631
        }
632
        $this->touch();
633
        $this->hasOneEmbeds->set($key, $embed);
634
        $this->doDirtyCheck();
635
        return $this;
636
    }
637
638
    /**
639
     * Touches the model.
640
     * Must be handled the the extending class.
641
     *
642
     * @param   bool    $force  Whether to force the load, even if the model is currently loaded.
643
     * @return  self
644
     */
645
    protected function touch($force = false)
646
    {
647
        return $this;
648
    }
649
650
    /**
651
     * Validates that the model type (from a Model or Collection instance) can be set to the relationship field.
652
     *
653
     * @param   string  $embedKey   The embed field key.
654
     * @param   string  $embedName  The embed name that is being set.
655
     * @return  self
656
     */
657
    protected function validateEmbedSet($embedKey, $embedName)
658
    {
659
        $embededPropMeta = $this->getMetadata()->getEmbed($embedKey);
660
        $this->getStore()->validateEmbedSet($embededPropMeta->embedMeta, $embedName);
661
        return $this;
662
    }
663
}
664