Store   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 666
Duplicated Lines 4.95 %

Coupling/Cohesion

Components 2
Dependencies 22

Importance

Changes 0
Metric Value
wmc 65
lcom 2
cbo 22
dl 33
loc 666
rs 1.4841
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A find() 0 8 2
A getModelTypes() 0 4 1
A findAll() 0 10 2
A findQuery() 0 11 1
A getModelCache() 0 4 1
A searchAutocomplete() 0 8 2
A create() 0 7 2
A delete() 0 5 1
A retrieveRecord() 0 9 2
A retrieveRecords() 0 5 1
A retrieveInverseRecords() 0 11 1
A loadModel() 0 14 1
A loadModels() 0 8 2
A dispatchLifecycleEvent() 0 6 1
A createModel() 0 14 3
A loadProxyModel() 0 12 2
A loadEmbed() 0 4 1
A createInverseCollection() 0 5 1
A createEmbedCollection() 0 16 4
A createCollection() 0 15 4
A getPersisterFor() 0 5 1
B loadCollection() 0 23 5
B commit() 0 26 5
A doCommitCreate() 12 12 1
A doCommitDelete() 10 10 1
A doCommitUpdate() 11 11 1
A validateEmbedSet() 0 7 2
A validateRelationshipSet() 0 12 3
A convertAttributeValue() 0 4 1
A shouldCommit() 0 5 3
A generateIdentifier() 0 4 1
A convertId() 0 4 1
A getMetadataForType() 0 4 1
A getMetadataForRelationship() 0 4 1
A isSequentialArray() 0 7 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Store often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Store, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace As3\Modlr\Store;
4
5
use As3\Modlr\DataTypes\TypeFactory;
6
use As3\Modlr\Events\EventDispatcher;
7
use As3\Modlr\Metadata\EmbeddedPropMetadata;
8
use As3\Modlr\Metadata\EmbedMetadata;
9
use As3\Modlr\Metadata\EntityMetadata;
10
use As3\Modlr\Metadata\MetadataFactory;
11
use As3\Modlr\Metadata\RelationshipMetadata;
12
use As3\Modlr\Models\Collections;
13
use As3\Modlr\Models\Embed;
14
use As3\Modlr\Models\Model;
15
use As3\Modlr\Persister\PersisterInterface;
16
use As3\Modlr\Persister\RecordSetInterface;
17
use As3\Modlr\StorageLayerManager;
18
use As3\Modlr\Store\Events\ModelLifecycleArguments;
19
use As3\Modlr\Store\Events\PreQueryArguments;
20
21
/**
22
 * Manages models and their persistence.
23
 *
24
 * @author Jacob Bare <[email protected]>
25
 */
26
class Store
27
{
28
    /**
29
     * @var MetadataFactory
30
     */
31
    private $mf;
32
33
    /**
34
     * @var TypeFactory
35
     */
36
    private $typeFactory;
37
38
    /**
39
     * The storage layer  manager.
40
     * Retrieves the appropriate persister and search client for handling records.
41
     *
42
     * @var  StorageLayerManager
43
     */
44
    private $storageManager;
45
46
    /**
47
     * Contains all models currently loaded in memory.
48
     *
49
     * @var Cache
50
     */
51
    private $cache;
52
53
    /**
54
     * The event dispatcher for firing model lifecycle events.
55
     *
56
     * @var EventDispatcher
57
     */
58
    private $dispatcher;
59
60
    /**
61
     * Constructor.
62
     *
63
     * @param   MetadataFactory         $mf
64
     * @param   StorageLayerManager     $storageManager
65
     * @param   TypeFactory             $typeFactory
66
     * @param   EventDispatcher         $dispatcher
67
     */
68
    public function __construct(MetadataFactory $mf, StorageLayerManager $storageManager, TypeFactory $typeFactory, EventDispatcher $dispatcher)
69
    {
70
        $this->mf = $mf;
71
        $this->storageManager = $storageManager;
72
        $this->typeFactory = $typeFactory;
73
        $this->dispatcher = $dispatcher;
74
        $this->cache = new Cache();
75
    }
76
77
    /**
78
     * Finds a single record from the persistence layer, by type and id.
79
     * Will return a Model object if found, or throw an exception if not.
80
     *
81
     * @api
82
     * @param   string  $typeKey    The model type.
83
     * @param   string  $identifier The model id.
84
     * @return  Model
85
     */
86
    public function find($typeKey, $identifier)
87
    {
88
        if (true === $this->cache->has($typeKey, $identifier)) {
89
            return $this->cache->get($typeKey, $identifier);
90
        }
91
        $record = $this->retrieveRecord($typeKey, $identifier);
92
        return $this->loadModel($typeKey, $record);
93
    }
94
95
    /**
96
     * Returns the available type keys from the MetadataFactory
97
     *
98
     * @return  array
99
     */
100
    public function getModelTypes()
101
    {
102
        return $this->mf->getAllTypeNames();
103
    }
104
105
    /**
106
     * Finds all records (or filtered by specific identifiers) for a type.
107
     *
108
     * @todo    Add sorting and pagination (limit/skip).
109
     * @todo    Handle find all with identifiers.
110
     * @param   string  $typeKey        The model type.
111
     * @param   array   $idenitifiers   The model identifiers (optional).
0 ignored issues
show
Documentation introduced by
There is no parameter named $idenitifiers. Did you maybe mean $identifiers?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
112
     * @param   array   $fields
113
     * @param   array   $sort
114
     * @param   int     $offset
115
     * @param   int     $limit
116
     * @return  Collections\Collection
117
     */
118
    public function findAll($typeKey, array $identifiers = [], array $fields = [], array $sort = [], $offset = 0, $limit = 0)
119
    {
120
        $metadata = $this->getMetadataForType($typeKey);
121
        if (!empty($identifiers)) {
122
            throw StoreException::nyi('Finding multiple records with specified identifiers is not yet supported.');
123
        }
124
        $recordSet = $this->retrieveRecords($typeKey, $identifiers, $fields, $sort, $offset, $limit);
125
        $models = $this->loadModels($typeKey, $recordSet);
126
        return new Collections\Collection($metadata, $this, $models, $recordSet->totalCount());
127
    }
128
129
    /**
130
     * Queries records based on a provided set of criteria.
131
     *
132
     * @param   string      $typeKey    The model type.
133
     * @param   array       $criteria   The query criteria.
134
     * @param   array       $fields     Fields to include/exclude.
135
     * @param   array       $sort       The sort criteria.
136
     * @param   int         $offset     The starting offset, aka the number of Models to skip.
137
     * @param   int         $limit      The number of Models to limit.
138
     * @return  Collections\Collection
139
     */
140
    public function findQuery($typeKey, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
141
    {
142
        $metadata = $this->getMetadataForType($typeKey);
143
        $persister = $this->getPersisterFor($typeKey);
144
        $this->dispatcher->dispatch(Events::preQuery, new PreQueryArguments($metadata, $this, $persister, $criteria));
145
146
        $recordSet = $persister->query($metadata, $this, $criteria, $fields, $sort, $offset, $limit);
147
148
        $models = $this->loadModels($typeKey, $recordSet);
149
        return new Collections\Collection($metadata, $this, $models, $recordSet->totalCount());
150
    }
151
152
    /**
153
     * Gets the model memory cache (identity map).
154
     *
155
     * @return  Cache
156
     */
157
    public function getModelCache()
158
    {
159
        return $this->cache;
160
    }
161
162
    /**
163
     * Searches for records (via the search layer) for a specific type, attribute, and value.
164
     * Uses the autocomplete logic to fullfill the request.
165
     *
166
     * @todo    Determine if full models should be return, or only specific fields.
167
     *          Autocompleters needs to be fast. If only specific fields are returned, do we need to exclude nulls in serialization?
168
     * @todo    Is search enabled for all models, by default, where everything is stored?
169
     *
170
     * @param   string  $typeKey
171
     * @param   string  $attributeKey
172
     * @param   string  $searchValue
173
     * @return  Collections\Collection
174
     */
175
    public function searchAutocomplete($typeKey, $attributeKey, $searchValue)
0 ignored issues
show
Unused Code introduced by
The parameter $attributeKey is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $searchValue is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
176
    {
177
        $metadata = $this->getMetadataForType($typeKey);
178
        if (false === $metadata->isSearchEnabled()) {
179
            throw StoreException::badRequest(sprintf('Search is not enabled for model type "%s"', $metadata->type));
180
        }
181
        return new Collections\Collection($metadata, $this, [], 0);
182
    }
183
184
    /**
185
     * Creates a new record.
186
     * The model will not be comitted to the persistence layer until $model->save() is called.
187
     *
188
     * @api
189
     * @param   string      $typeKey    The model type.
190
     * @param   string|null $identifier The model identifier. Generally should be null unless client-side id generation is in place.
191
     * @return  Model
192
     */
193
    public function create($typeKey, $identifier = null)
194
    {
195
        if (empty($identifier)) {
196
            $identifier = $this->generateIdentifier($typeKey);
197
        }
198
        return $this->createModel($typeKey, $identifier);
199
    }
200
201
    /**
202
     * Deletes a model.
203
     * The moel will be immediately deleted once retrieved.
204
     *
205
     * @api
206
     * @param   string      $typeKey    The model type.
207
     * @param   string|null $identifier The model identifier.
208
     * @return  Model
209
     */
210
    public function delete($typeKey, $identifier)
211
    {
212
        $model = $this->find($typeKey, $identifier);
213
        return $model->delete()->save();
214
    }
215
216
    /**
217
     * Retrieves a RecordSet object from the persistence layer.
218
     *
219
     * @param   string  $typeKey    The model type.
220
     * @param   string  $identifier The model identifier.
221
     * @return  array
222
     * @throws  StoreException  If the record cannot be found.
223
     */
224
    public function retrieveRecord($typeKey, $identifier)
225
    {
226
        $persister = $this->getPersisterFor($typeKey);
227
        $record = $persister->retrieve($this->getMetadataForType($typeKey), $identifier, $this)->getSingleResult();
228
        if (null === $record) {
229
            throw StoreException::recordNotFound($typeKey, $identifier);
230
        }
231
        return $record;
232
    }
233
234
    /**
235
     * Retrieves multiple Record objects from the persistence layer.
236
     *
237
     * @todo    Implement sorting and pagination (limit/skip).
238
     * @param   string  $typeKey        The model type.
239
     * @param   array   $identifiers    The model identifier.
240
     * @param   array   $fields
241
     * @param   array   $sort
242
     * @param   int     $offset
243
     * @param   int     $limit
244
     * @return  RecordSetInterface
245
     */
246
    public function retrieveRecords($typeKey, array $identifiers, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
247
    {
248
        $persister = $this->getPersisterFor($typeKey);
249
        return $persister->all($this->getMetadataForType($typeKey), $this, $identifiers, $fields, $sort, $offset, $limit);
250
    }
251
252
    /**
253
     * Retrieves multiple Record objects from the persistence layer for an inverse relationship.
254
     *
255
     * @todo    Need to find a way to query all inverse at the same time for a findAll query, as it's queried multiple times.
256
     * @param   string  $ownerTypeKey
257
     * @param   string  $relTypeKey
258
     * @param   array   $identifiers
259
     * @param   string  $inverseField
260
     * @return  RecordSetInterface
261
     */
262
    public function retrieveInverseRecords($ownerTypeKey, $relTypeKey, array $identifiers, $inverseField)
263
    {
264
        $persister = $this->getPersisterFor($relTypeKey);
265
        return $persister->inverse(
266
            $this->getMetadataForType($ownerTypeKey),
267
            $this->getMetadataForType($relTypeKey),
268
            $this,
269
            $identifiers,
270
            $inverseField
271
        );
272
    }
273
274
    /**
275
     * Loads/creates a model from a persistence layer Record.
276
     *
277
     * @param   string  $typeKey    The model type.
278
     * @param   array   $record     The persistence layer record.
279
     * @return  Model
280
     */
281
    protected function loadModel($typeKey, array $record)
282
    {
283
        $this->mf->validateResourceTypes($typeKey, $record['type']);
284
        // Must use the type from the record to cover polymorphic models.
285
        $metadata = $this->getMetadataForType($record['type']);
286
287
        $model = new Model($metadata, $record['identifier'], $this, $record['properties']);
288
        $model->getState()->setLoaded();
289
290
        $this->dispatchLifecycleEvent(Events::postLoad, $model);
291
292
        $this->cache->push($model);
293
        return $model;
294
    }
295
296
    /**
297
     * Loads/creates multiple models from persistence layer Records.
298
     *
299
     * @param   string              $typeKey    The model type.
300
     * @param   RecordSetInterface  $records    The persistence layer records.
301
     * @return  Model[]
302
     */
303
    protected function loadModels($typeKey, RecordSetInterface $records)
304
    {
305
        $models = [];
306
        foreach ($records as $record) {
307
            $models[] = $this->loadModel($typeKey, $record);
308
        }
309
        return $models;
310
    }
311
312
    /**
313
     * Dispatches a model lifecycle event via the event dispatcher.
314
     *
315
     * @param   string  $eventName
316
     * @param   Model   $model
317
     * @return  self
318
     */
319
    protected function dispatchLifecycleEvent($eventName, Model $model)
320
    {
321
        $args = new ModelLifecycleArguments($model);
322
        $this->dispatcher->dispatch($eventName, $args);
323
        return $this;
324
    }
325
326
    /**
327
     * Creates a new Model instance.
328
     * Will not be persisted until $model->save() is called.
329
     *
330
     * @param   string  $typeKey    The model type.
331
     * @param   string  $identifier The model identifier.
332
     * @return  Model
333
     */
334
    protected function createModel($typeKey, $identifier)
335
    {
336
        if (true === $this->cache->has($typeKey, $identifier)) {
337
            throw new \RuntimeException(sprintf('A model is already loaded for type "%s" using identifier "%s"', $typeKey, $identifier));
338
        }
339
        $metadata = $this->getMetadataForType($typeKey);
340
        if (true === $metadata->isAbstract()) {
341
            throw StoreException::badRequest('Abstract models cannot be created directly. You must instantiate a child class');
342
        }
343
        $model = new Model($metadata, $identifier, $this);
344
        $model->getState()->setNew();
345
        $this->cache->push($model);
346
        return $model;
347
    }
348
349
    /**
350
     * Loads a has-one model proxy.
351
     *
352
     * @param   string  $relatedTypeKey
353
     * @param   string  $identifier
354
     * @return  Model
355
     */
356
    public function loadProxyModel($relatedTypeKey, $identifier)
357
    {
358
        $identifier = $this->convertId($identifier);
359
        if (true === $this->cache->has($relatedTypeKey, $identifier)) {
360
            return $this->cache->get($relatedTypeKey, $identifier);
361
        }
362
363
        $metadata = $this->getMetadataForType($relatedTypeKey);
364
        $model = new Model($metadata, $identifier, $this);
365
        $this->cache->push($model);
366
        return $model;
367
    }
368
369
    /**
370
     * Loads an Embed model
371
     *
372
     * @param   EmbedMetadata   $embedMeta
373
     * @param   array           $embed
374
     * @return  Embed
375
     */
376
    public function loadEmbed(EmbedMetadata $embedMeta, array $embed)
377
    {
378
        return new Embed($embedMeta, $this, $embed);
379
    }
380
381
    /**
382
     * Loads a has-many inverse model collection.
383
     *
384
     * @param   RelationshipMetadata    $relMeta
385
     * @param   Model                   $owner
386
     * @return  Collections\InverseCollection
387
     */
388
    public function createInverseCollection(RelationshipMetadata $relMeta, Model $owner)
389
    {
390
        $metadata = $this->getMetadataForType($relMeta->getEntityType());
391
        return new Collections\InverseCollection($metadata, $this, $owner, $relMeta->inverseField);
0 ignored issues
show
Documentation introduced by
$relMeta->inverseField is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
392
    }
393
394
    /**
395
     * Loads a has-many embed collection.
396
     *
397
     * @param   EmbeddedPropMetadata    $relMeta
0 ignored issues
show
Bug introduced by
There is no parameter named $relMeta. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
398
     * @param   array|null              $embedDocs
399
     * @return  Collections\EmbedCollection
400
     */
401
    public function createEmbedCollection(EmbeddedPropMetadata $embededPropMeta, array $embedDocs = null)
402
    {
403
        if (empty($embedDocs)) {
404
            $embedDocs = [];
405
        }
406
        if (false === $this->isSequentialArray($embedDocs)) {
407
            throw StoreException::badRequest(sprintf('Improper has-many data detected for embed "%s" - a sequential array is required.', $embededPropMeta->getKey()));
408
        }
409
410
        $embeds = [];
411
        foreach ($embedDocs as $embedDoc) {
412
            $embeds[] = $this->loadEmbed($embededPropMeta->embedMeta, $embedDoc);
413
        }
414
415
        return new Collections\EmbedCollection($embededPropMeta->embedMeta, $this, $embeds);
416
    }
417
418
    /**
419
     * Loads a has-many model collection.
420
     *
421
     * @param   RelationshipMetadata    $relMeta
422
     * @param   array|null              $references
423
     * @return  Collections\Collection
424
     */
425
    public function createCollection(RelationshipMetadata $relMeta, array $references = null)
426
    {
427
        $metadata = $this->getMetadataForType($relMeta->getEntityType());
428
        if (empty($references)) {
429
            $references = [];
430
        }
431
        if (false === $this->isSequentialArray($references)) {
432
            throw StoreException::badRequest(sprintf('Improper has-many data detected for relationship "%s" - a sequential array is required.', $relMeta->getKey()));
433
        }
434
        $models = [];
435
        foreach ($references as $reference) {
436
            $models[] = $this->loadProxyModel($reference['type'], $reference['id']);
437
        }
438
        return new Collections\Collection($metadata, $this, $models, count($models));
439
    }
440
441
    /**
442
     * Determines the persister to use for the provided model key.
443
     *
444
     * @param   string  $typeKey    The model type key.
445
     * @return  PersisterInterface
446
     */
447
    public function getPersisterFor($typeKey)
448
    {
449
        $metadata = $this->getMetadataForType($typeKey);
450
        return $this->storageManager->getPersister($metadata->persistence->getKey());
451
    }
452
453
    /**
454
     * Loads/fills a collection of empty (unloaded) models with data from the persistence layer.
455
     *
456
     * @param   Collections\AbstractCollection  $collection
457
     * @return  Model[]
458
     */
459
    public function loadCollection(Collections\AbstractCollection $collection)
460
    {
461
        $identifiers = $collection->getIdentifiers();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class As3\Modlr\Models\Collections\AbstractCollection as the method getIdentifiers() does only exist in the following sub-classes of As3\Modlr\Models\Collections\AbstractCollection: As3\Modlr\Models\Collections\Collection, As3\Modlr\Models\Collections\InverseCollection, As3\Modlr\Models\Collections\ModelCollection. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
462
        if (empty($identifiers)) {
463
            // Nothing to query.
464
            return [];
465
        }
466
        if ($collection instanceof Collections\InverseCollection) {
467
            $recordSet = $this->retrieveInverseRecords($collection->getOwner()->getType(), $collection->getType(), $collection->getIdentifiers(), $collection->getQueryField());
468
        } else {
469
            $recordSet = $this->retrieveRecords($collection->getType(), $collection->getIdentifiers());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class As3\Modlr\Models\Collections\AbstractCollection as the method getIdentifiers() does only exist in the following sub-classes of As3\Modlr\Models\Collections\AbstractCollection: As3\Modlr\Models\Collections\Collection, As3\Modlr\Models\Collections\InverseCollection, As3\Modlr\Models\Collections\ModelCollection. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
470
        }
471
472
        $models = [];
473
        foreach ($recordSet as $record) {
474
            if (true === $this->cache->has($record['type'], $record['identifier'])) {
475
                $models[] = $this->cache->get($record['type'], $record['identifier']);
476
                continue;
477
            }
478
            $models[] = $this->loadModel($collection->getType(), $record);
479
        }
480
        return $models;
481
    }
482
483
    /**
484
     * Commits a model by persisting it to the database.
485
     *
486
     * @todo    Eventually we'll want to schedule models and allow for mutiple commits, flushes, etc.
487
     * @todo    Will need to handle cascade saving of new or modified relationships??
488
     * @param   Model   $model  The model to commit.
489
     * @return  Model
490
     */
491
    public function commit(Model $model)
492
    {
493
        $this->dispatchLifecycleEvent(Events::preCommit, $model);
494
495
        if (false === $this->shouldCommit($model)) {
496
            return $model;
497
        }
498
499
        if (true === $model->getState()->is('new')) {
500
            $this->doCommitCreate($model);
501
502
        } elseif (true === $model->getState()->is('deleting')) {
503
            // Deletes must execute before updates to prevent an update then a delete.
504
            $this->doCommitDelete($model);
505
506
        } elseif (true === $model->isDirty()) {
507
            $this->doCommitUpdate($model);
508
509
        } else {
510
            throw new \RuntimeException('Unable to commit model.');
511
        }
512
513
        $this->dispatchLifecycleEvent(Events::postCommit, $model);
514
515
        return $model;
516
    }
517
518
    /**
519
     * Performs a Model creation commit and persists to the database.
520
     *
521
     * @param   Model   $model
522
     * @return  Model
523
     */
524 View Code Duplication
    private function doCommitCreate(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...
525
    {
526
        $this->dispatchLifecycleEvent(Events::preCreate, $model);
527
528
        $this->getPersisterFor($model->getType())->create($model);
529
        $model->getState()->setNew(false);
530
        // Should the model always reload? Or should the commit be assumed correct and just clear the new/dirty state?
531
        $model->reload();
532
533
        $this->dispatchLifecycleEvent(Events::postCreate, $model);
534
        return $model;
535
    }
536
537
    /**
538
     * Performs a Model delete commit and persists to the database.
539
     *
540
     * @param   Model   $model
541
     * @return  Model
542
     */
543 View Code Duplication
    private function doCommitDelete(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...
544
    {
545
        $this->dispatchLifecycleEvent(Events::preDelete, $model);
546
547
        $this->getPersisterFor($model->getType())->delete($model);
548
        $model->getState()->setDeleted();
549
550
        $this->dispatchLifecycleEvent(Events::postDelete, $model);
551
        return $model;
552
    }
553
554
    /**
555
     * Performs a Model update commit and persists to the database.
556
     *
557
     * @param   Model   $model
558
     * @return  Model
559
     */
560 View Code Duplication
    private function doCommitUpdate(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...
561
    {
562
        $this->dispatchLifecycleEvent(Events::preUpdate, $model);
563
564
        $this->getPersisterFor($model->getType())->update($model);
565
        // Should the model always reload? Or should the commit be assumed correct and just clear the new/dirty state?
566
        $model->reload();
567
568
        $this->dispatchLifecycleEvent(Events::postUpdate, $model);
569
        return $model;
570
    }
571
572
    /**
573
     * Validates that an embed name/type can be set to an owning embed metadata type.
574
     *
575
     * @param   EmbedMetadata   $owningMeta     The metadata the type will be added to.
576
     * @param   string          $nameToCheck    The name to check.
577
     * @return  self
578
     * @throws  StoreException  If the type to add is not supported.
579
     */
580
    public function validateEmbedSet(EmbedMetadata $owningMeta, $nameToCheck)
581
    {
582
        if ($owningMeta->name !== $nameToCheck) {
583
            throw StoreException::badRequest(sprintf('The embed type "%s" cannot be added to "%s", as it is not supported.', $nameToCheck, $owningMeta->name));
584
        }
585
        return $this;
586
    }
587
588
    /**
589
     * Validates that a model type can be set to an owning metadata type.
590
     *
591
     * @param   EntityMetadata  $owningMeta The metadata the type will be added to.
592
     * @param   string          $typeToAdd  The type to add.
593
     * @return  self
594
     * @throws  StoreException  If the type to add is not supported.
595
     */
596
    public function validateRelationshipSet(EntityMetadata $owningMeta, $typeToAdd)
597
    {
598
        if (true === $owningMeta->isPolymorphic()) {
599
            $canSet = in_array($typeToAdd, $owningMeta->ownedTypes);
600
        } else {
601
            $canSet = $owningMeta->type === $typeToAdd;
602
        }
603
        if (false === $canSet) {
604
            throw StoreException::badRequest(sprintf('The model type "%s" cannot be added to "%s", as it is not supported.', $typeToAdd, $owningMeta->type));
605
        }
606
        return $this;
607
    }
608
609
    /**
610
     * Converts an attribute value to the proper Modlr data type.
611
     *
612
     * @param   string  $dataType   The data type, such as string, integer, boolean, etc.
613
     * @param   mixed   $value      The value to convert.
614
     * @return  mixed
615
     */
616
    public function convertAttributeValue($dataType, $value)
617
    {
618
        return $this->typeFactory->convertToModlrValue($dataType, $value);
619
    }
620
621
    /**
622
     * Determines if a model is eligible for commit.
623
     *
624
     * @todo    Does delete need to be here?
625
     * @param   Model   $model
626
     * @return  bool
627
     */
628
    protected function shouldCommit(Model $model)
629
    {
630
        $state = $model->getState();
631
        return $model->isDirty() || $state->is('new') || $state->is('deleting');
632
    }
633
634
    /**
635
     * Generates a new identifier value for a model type.
636
     *
637
     * @param   string  $typeKey    The model type.
638
     * @return  string
639
     */
640
    protected function generateIdentifier($typeKey)
641
    {
642
        return $this->convertId($this->getPersisterFor($typeKey)->generateId());
643
    }
644
645
    /**
646
     * Converts the id value to a normalized string.
647
     *
648
     * @param   mixed   $identenfier    The identifier to convert.
0 ignored issues
show
Bug introduced by
There is no parameter named $identenfier. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
649
     * @return  string
650
     */
651
    protected function convertId($identifier)
652
    {
653
        return (String) $identifier;
654
    }
655
656
    /**
657
     * Gets the metadata for a model type.
658
     *
659
     * @param   string  $typeKey    The model type.
660
     * @return  EntityMetadata
661
     */
662
    public function getMetadataForType($typeKey)
663
    {
664
        return $this->mf->getMetadataForType($typeKey);
665
    }
666
667
    /**
668
     * Gets the metadata for a relationship.
669
     *
670
     * @param   RelationshipMetadata    $relMeta    The relationship metadata.
671
     * @return  EntityMetadata
672
     */
673
    public function getMetadataForRelationship(RelationshipMetadata $relMeta)
674
    {
675
        return $this->getMetadataForType($relMeta->getEntityType());
676
    }
677
678
    /**
679
     * Determines if an array is sequential.
680
     *
681
     * @param   array   $arr
682
     * @return  bool
683
     */
684
    protected function isSequentialArray(array $arr)
685
    {
686
        if (empty($arr)) {
687
            return true;
688
        }
689
        return (range(0, count($arr) - 1) === array_keys($arr));
690
    }
691
}
692