Completed
Pull Request — master (#79)
by Jacob
02:58
created

Model::rollback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
1
<?php
2
3
namespace As3\Modlr\Models;
4
5
use As3\Modlr\Models\Relationships;
6
use As3\Modlr\Persister\Record;
7
use As3\Modlr\Store\Store;
8
use As3\Modlr\Metadata\EntityMetadata;
9
10
/**
11
 * Represents a data record from a persistence (database) layer.
12
 *
13
 * @author Jacob Bare <[email protected]>
14
 */
15
class Model extends AbstractModel
16
{
17
    /**
18
     * The id value of this model.
19
     * Always converted to a string when in the model context.
20
     *
21
     * @var string
22
     */
23
    protected $identifier;
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
     * Enables/disables collection auto-initialization on iteration.
41
     * Will not load/fill the collection from the database if false.
42
     * Is useful for large hasMany iterations where only id and type are required (ala serialization).
43
     *
44
     * @var bool
45
     */
46
    protected $collectionAutoInit = true;
47
48
    /**
49
     * Constructor.
50
     *
51
     * @param   EntityMetadata  $metadata       The internal entity metadata that supports this Model.
52
     * @param   string          $identifier     The database identifier.
53
     * @param   Store           $store          The model store service for handling persistence operations.
54
     * @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.
55
     */
56
    public function __construct(EntityMetadata $metadata, $identifier, Store $store, array $properties = null)
57
    {
58
        $this->identifier = $identifier;
59
        parent::__construct($metadata, $store, $properties);
60
    }
61
62
    /**
63
     * Cloner.
64
     * Ensures sub objects are also cloned.
65
     *
66
     */
67
    public function __clone()
68
    {
69
        parent::__clone();
70
        $this->hasOneRelationships = clone $this->hasOneRelationships;
71
        $this->hasManyRelationships = clone $this->hasManyRelationships;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     *
77
     * Overloaded to support relationships.
78
     *
79
     */
80
    public function apply(array $properties)
81
    {
82
        foreach ($properties as $key => $value) {
83
            if (true === $this->isHasOne($key)) {
84
                if (empty($value)) {
85
                    $this->clear($key);
86
                    continue;
87
                }
88
                $value = $this->store->loadProxyModel($value['type'], $value['id']);
89
                $this->set($key, $value);
90
                continue;
91
            }
92
93
        }
94
95
        foreach ($this->getMetadata()->getRelationships() as $key => $relMeta) {
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 getRelationships() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
96
            if (true === $relMeta->isOne()) {
97
                continue;
98
            }
99
            if (!isset($properties[$key]) || true === $relMeta->isInverse) {
100
                continue;
101
            }
102
            $this->clear($key);
103
            $collection = $this->store->createCollection($relMeta, $properties[$key]);
104
            foreach ($collection->allWithoutLoad() as $value) {
105
                $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...
106
            }
107
        }
108
        return parent::apply($properties);
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     *
114
     * Overloaded to support relationships.
115
     */
116 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...
117
    {
118
        if (true === $this->isHasOne($key)) {
119
            return $this->setHasOne($key, null);
120
        }
121
        if (true === $this->isInverse($key)) {
122
            throw ModelException::cannotModifyInverse($this, $key);
123
        }
124
        if (true === $this->isHasMany($key)) {
125
            $collection = $this->hasManyRelationships->get($key);
126
            $collection->clear();
127
            $this->doDirtyCheck();
128
            return $this;
129
        }
130
        return parent::clear($key);
131
    }
132
133
    /**
134
     * Enables or disables has-many collection auto-initialization from the database.
135
     *
136
     * @param   bool    $bit    Whether to enable/disable.
137
     * @return  self
138
     */
139
    public function enableCollectionAutoInit($bit = true)
140
    {
141
        $this->collectionAutoInit = (Boolean) $bit;
142
        return $this;
143
    }
144
145
    /**
146
     * Marks the record for deletion.
147
     * Will not remove from the database until $this->save() is called.
148
     *
149
     * @api
150
     * @return  self
151
     * @throws  \RuntimeException   If a new (unsaved) model is deleted.
152
     */
153
    public function delete()
154
    {
155
        if (true === $this->getState()->is('new')) {
156
            throw new \RuntimeException('You cannot delete a new model');
157
        }
158
        if (true === $this->getState()->is('deleted')) {
159
            return $this;
160
        }
161
        $this->getState()->setDeleting();
162
        return $this;
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     *
168
     * Overloaded to support relationships.
169
     *
170
     */
171
    public function get($key)
172
    {
173
        if (true === $this->isRelationship($key)) {
174
            return $this->getRelationship($key);
175
        }
176
        return parent::get($key);
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     *
182
     * Overloaded to support relationships.
183
     *
184
     */
185
    public function getChangeSet()
186
    {
187
        $changeset = parent::getChangeSet();
188
        $changeset['hasOne']  = $this->filterNotSavedProperties($this->hasOneRelationships->calculateChangeSet());
189
        $changeset['hasMany'] = $this->filterNotSavedProperties($this->hasManyRelationships->calculateChangeSet());
190
        return $changeset;
191
    }
192
193
    /**
194
     * Gets the composite key of the model by combining the model type with the unique id.
195
     *
196
     * @api
197
     * @return  string
198
     */
199
    public function getCompositeKey()
200
    {
201
        return sprintf('%s.%s', $this->getType(), $this->getId());
202
    }
203
204
    /**
205
     * Gets the unique identifier of this model.
206
     *
207
     * @api
208
     * @return  string
209
     */
210
    public function getId()
211
    {
212
        return $this->identifier;
213
    }
214
215
    /**
216
     * Gets the model type.
217
     *
218
     * @api
219
     * @return  string
220
     */
221
    public function getType()
222
    {
223
        return $this->metadata->type;
0 ignored issues
show
Bug introduced by
Accessing type on the interface As3\Modlr\Metadata\Interfaces\AttributeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     *
229
     * Overloaded to support relationships.
230
     *
231
     */
232
    public function initialize(array $properties = null)
233
    {
234
        $hasOne = [];
235
        $hasMany = [];
236
237
        if (null !== $properties) {
238
            foreach ($properties as $key => $value) {
239
                if (true === $this->isHasOne($key)) {
240
                    // Load hasOne relationship.
241
                    $hasOne[$key] = $this->getStore()->loadProxyModel($value['type'], $value['id']);
242
                    continue;
243
                }
244
            }
245
        }
246
247
        foreach ($this->getMetadata()->getRelationships() as $key => $relMeta) {
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 getRelationships() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
248
            if (true === $relMeta->isOne()) {
249
                continue;
250
            }
251
            if (true === $relMeta->isInverse) {
252
                $hasMany[$key] = $this->getStore()->createInverseCollection($relMeta, $this);
253
            } else {
254
                $references = !isset($properties[$key]) ? [] : $properties[$key];
255
                $hasMany[$key] = $this->getStore()->createCollection($relMeta, $references);
256
            }
257
        }
258
259
        $this->hasOneRelationships  = (null === $this->hasOneRelationships) ? new Relationships\HasOne($hasOne) : $this->hasOneRelationships->replace($hasOne);
260
        $this->hasManyRelationships = (null === $this->hasManyRelationships) ? new Relationships\HasMany($hasMany) : $this->hasManyRelationships->replace($hasMany);
261
        return parent::initialize($properties);
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     *
267
     * Overloaded to support relationships.
268
     *
269
     */
270
    public function isDirty()
271
    {
272
        return true === parent::isDirty()
273
            || true === $this->hasOneRelationships->areDirty()
274
            || true === $this->hasManyRelationships->areDirty()
275
        ;
276
    }
277
278
    /**
279
     * Determines if a property key is a has-many relationship.
280
     *
281
     * @api
282
     * @param   string  $key    The property key.
283
     * @return  bool
284
     */
285
    public function isHasMany($key)
286
    {
287
        if (false === $this->isRelationship($key)) {
288
            return false;
289
        }
290
        return $this->getMetadata()->getRelationship($key)->isMany();
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 getRelationship() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
291
    }
292
293
    /**
294
     * Determines if a property key is a has-one relationship.
295
     *
296
     * @api
297
     * @param   string  $key    The property key.
298
     * @return  bool
299
     */
300
    public function isHasOne($key)
301
    {
302
        if (false === $this->isRelationship($key)) {
303
            return false;
304
        }
305
        return $this->getMetadata()->getRelationship($key)->isOne();
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 getRelationship() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
306
    }
307
308
    /**
309
     * Determines if a property key is a an inverse relationship.
310
     *
311
     * @api
312
     * @param   string  $key    The property key.
313
     * @return  bool
314
     */
315
    public function isInverse($key)
316
    {
317
        if (false === $this->isRelationship($key)) {
318
            return false;
319
        }
320
        return $this->getMetadata()->getRelationship($key)->isInverse;
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 getRelationship() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
321
    }
322
323
    /**
324
     * Determines if a property key is a relationship (either has-one or has-many).
325
     *
326
     * @api
327
     * @param   string  $key    The property key.
328
     * @return  bool
329
     */
330
    public function isRelationship($key)
331
    {
332
        return $this->getMetadata()->hasRelationship($key);
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 hasRelationship() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
333
    }
334
335
    /**
336
     * Pushes a Model into a has-many relationship collection.
337
     * This method must be used for has-many relationships. Direct set is not supported.
338
     * To completely replace a has-many, call clear() first and then push() the new Models.
339
     *
340
     * @api
341
     * @param   string  $key
342
     * @param   Model   $model
343
     * @return  self
344
     */
345 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...
346
    {
347
        if (true === $this->isHasOne($key)) {
348
            return $this->setHasOne($key, $model);
349
        }
350
        if (false === $this->isHasMany($key)) {
351
            return $this;
352
        }
353
        if (true === $this->isInverse($key)) {
354
            throw ModelException::cannotModifyInverse($this, $key);
355
        }
356
        $this->touch();
357
        $collection = $this->hasManyRelationships->get($key);
358
        $collection->push($model);
359
        $this->doDirtyCheck();
360
        return $this;
361
    }
362
363
    /**
364
     * Reloads the model from the database.
365
     *
366
     * @api
367
     * @return  self
368
     */
369
    public function reload()
370
    {
371
        return $this->touch(true);
372
    }
373
374
    /**
375
     * Removes a specific Model from a has-many relationship collection.
376
     *
377
     * @api
378
     * @param   string  $key    The has-many relationship key.
379
     * @param   Model   $model  The model to remove from the collection.
380
     * @return  self
381
     */
382 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...
383
    {
384
        if (false === $this->isHasMany($key)) {
385
            return $this;
386
        }
387
        if (true === $this->isInverse($key)) {
388
            throw ModelException::cannotModifyInverse($this, $key);
389
        }
390
        $this->touch();
391
        $collection = $this->hasManyRelationships->get($key);
392
        $collection->remove($model);
393
        $this->doDirtyCheck();
394
        return $this;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     * Overloaded to support relationship rollback.
400
     */
401
    public function rollback()
402
    {
403
        $this->hasOneRelationships->rollback();
404
        $this->hasManyRelationships->rollback();
405
        return parent::rollback();
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     *
411
     * Overloaded to support relationships.
412
     * Sets a model property: an attribute value, a has-one model, or an entire has-many model collection.
413
     * Note: To push/remove a single Model into a has-many collection, or clear a collection, use @see push(), remove() and clear().
414
     *
415
     */
416
    public function set($key, $value)
417
    {
418
        if (true === $this->isRelationship($key)) {
419
            return $this->setRelationship($key, $value);
420
        }
421
        return parent::set($key, $value);
422
    }
423
424
    /**
425
     * Saves the model.
426
     *
427
     * @api
428
     * @param   Implement cascade relationship saves. Or should the store handle this?
429
     * @return  self
430
     */
431
    public function save()
432
    {
433
        if (true === $this->getState()->is('deleted')) {
434
            return $this;
435
        }
436
        $this->store->commit($this);
437
        return $this;
438
    }
439
440
    /**
441
     * {@inheritdoc}
442
     *
443
     * Overloaded to support relationships.
444
     */
445
    protected function filterNotSavedProperties(array $properties)
446
    {
447 View Code Duplication
        foreach ($this->getMetadata()->getRelationships() as $fieldKey => $propMeta) {
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 getRelationships() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
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...
448
            if (true === $propMeta->shouldSave() || !isset($properties[$fieldKey])) {
449
                continue;
450
            }
451
            unset($properties[$fieldKey]);
452
        }
453
        return parent::filterNotSavedProperties($properties);
454
    }
455
456
    /**
457
     * {@inheritdoc}
458
     *
459
     * Overloaded to support global model defaults.
460
     *
461
     */
462
    protected function applyDefaultAttrValues(array $attributes = [])
463
    {
464
        $attributes = parent::applyDefaultAttrValues($attributes);
465
466
        // Set defaults for the entire entity.
467
        foreach ($this->getMetadata()->defaultValues as $key => $value) {
0 ignored issues
show
Bug introduced by
Accessing defaultValues on the interface As3\Modlr\Metadata\Interfaces\AttributeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
468
            if (isset($attributes[$key])) {
469
                continue;
470
            }
471
            $attributes[$key] = $this->convertAttributeValue($key, $value);
472
        }
473
        return $attributes;
474
    }
475
476
    /**
477
     * Gets a relationship value.
478
     *
479
     * @param   string  $key    The relationship key (field) name.
480
     * @return  Model|array|null
481
     * @throws  \RuntimeException If hasMany relationships are accessed directly.
482
     */
483
    protected function getRelationship($key)
484
    {
485
        if (true === $this->isHasOne($key)) {
486
            $this->touch();
487
            return $this->hasOneRelationships->get($key);
488
        }
489
        if (true === $this->isHasMany($key)) {
490
            $this->touch();
491
            $collection = $this->hasManyRelationships->get($key);
492
            if ($collection->isLoaded($collection)) {
493
                return iterator_to_array($collection);
494
            }
495
            return (true === $this->collectionAutoInit) ? iterator_to_array($collection) : $collection->allWithoutLoad();
496
        }
497
        return null;
498
    }
499
500
    /**
501
     * Sets a has-one relationship.
502
     *
503
     * @param   string      $key    The relationship key (field) name.
504
     * @param   Model|null  $model  The model to relate.
505
     * @return  self
506
     */
507 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...
508
    {
509
        if (true === $this->isInverse($key)) {
510
            throw ModelException::cannotModifyInverse($this, $key);
511
        }
512
        if (null !== $model) {
513
            $this->validateRelSet($key, $model->getType());
514
        }
515
        $this->touch();
516
        $this->hasOneRelationships->set($key, $model);
517
        $this->doDirtyCheck();
518
        return $this;
519
    }
520
521
    /**
522
     * Sets a relationship value.
523
     *
524
     * @param   string      $key
525
     * @param   Model|null  $value
526
     * @return  self
527
     */
528
    protected function setRelationship($key, $value)
529
    {
530
        if (true === $this->isHasOne($key)) {
531
            return $this->setHasOne($key, $value);
532
        }
533
        if (true === $this->isHasMany($key)) {
534
            throw new \RuntimeException('You cannot set a hasMany relationship directly. Please access using push(), clear(), and/or remove()');
535
        }
536
        return $this;
537
    }
538
539
    /**
540
     * {@inheritdoc}
541
     *
542
     * Overloaded to handle loading from the database.
543
     * If the model is currently empty, it will query the database and fill/load the model.
544
     *
545
     */
546
    protected function touch($force = false)
547
    {
548
        if (true === $this->getState()->is('deleted')) {
549
            return $this;
550
        }
551
        if (true === $this->getState()->is('empty') || true === $force) {
552
            $record = $this->store->retrieveRecord($this->getType(), $this->getId());
553
            $this->initialize($record->getProperties());
554
            $this->getState()->setLoaded();
555
        }
556
        return $this;
557
    }
558
559
    /**
560
     * Validates that the model type (from a Model or Collection instance) can be set to the relationship field.
561
     *
562
     * @param   string  $relKey The relationship field key.
563
     * @param   string  $type   The model type that is being related.
564
     * @return  self
565
     */
566
    protected function validateRelSet($relKey, $type)
567
    {
568
        $relMeta = $this->getMetadata()->getRelationship($relKey);
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 getRelationship() does only exist in the following implementations of said interface: As3\Modlr\Metadata\EntityMetadata, As3\Modlr\Metadata\MixinMetadata.

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...
569
        $relatedModelMeta = $this->getStore()->getMetadataForRelationship($relMeta);
570
        $this->getStore()->validateRelationshipSet($relatedModelMeta, $type);
571
        return $this;
572
    }
573
}
574