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

AbstractCollection   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 424
Duplicated Lines 8.73 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 45
c 2
b 1
f 0
lcom 1
cbo 2
dl 37
loc 424
rs 8.3673

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A calculateChangeSet() 0 10 4
A clear() 0 7 1
A count() 0 4 1
A current() 0 4 1
A getSingleResult() 0 8 2
getType() 0 1 ?
A has() 0 4 1
A hasDirtyModels() 0 9 3
A isDirty() 0 4 2
A isEmpty() 0 4 1
A isLoaded() 0 4 1
A key() 0 4 1
A next() 0 4 1
A push() 18 18 4
A remove() 19 19 4
A rewind() 0 4 1
A rollback() 0 7 1
A valid() 0 4 1
A willAdd() 0 4 1
A willRemove() 0 4 1
A add() 0 15 4
A evict() 0 8 1
A hasOriginal() 0 4 1
A indexOf() 0 12 3
modelsMatch() 0 1 ?
A set() 0 7 1
A setModels() 0 7 2
validateAdd() 0 1 ?

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 AbstractCollection 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 AbstractCollection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace As3\Modlr\Models\Collections;
4
5
use \Countable;
6
use \Iterator;
7
use As3\Modlr\Models\AbstractModel;
8
use As3\Modlr\Store\Store;
9
10
/**
11
 * Collection that contains record representations from a persistence (database) layer.
12
 * These representations can either be of first-order Models or fragmentted Embeds.
13
 *
14
 * @author Jacob Bare <[email protected]>
15
 */
16
abstract class AbstractCollection implements Iterator, Countable
17
{
18
    /**
19
     * Models added to this collection.
20
     * Tracks newly added models for rollback/change purposes.
21
     *
22
     * @var AbstractModel[]
23
     */
24
    protected $added = [];
25
26
    /**
27
     * Whether the collection has been loaded with data from the persistence layer
28
     *
29
     * @var bool
30
     */
31
    protected $loaded = true;
32
33
    /**
34
     * Current models assigned to this collection.
35
     * Needed for iteration, access, and count purposes.
36
     *
37
     * @var AbstractModel[]
38
     */
39
    protected $models = [];
40
41
    /**
42
     * Original models assigned to this collection.
43
     *
44
     * @var AbstractModel[]
45
     */
46
    protected $original = [];
47
48
    /**
49
     * The array position.
50
     *
51
     * @var int
52
     */
53
    protected $pos = 0;
54
55
    /**
56
     * Models removed from this collection.
57
     * Tracks removed models for rollback/change purposes.
58
     *
59
     * @var AbstractModel[]
60
     */
61
    protected $removed = [];
62
63
    /**
64
     * The store for handling storage operations.
65
     *
66
     * @var Store
67
     */
68
    protected $store;
69
70
    /**
71
     * Constructor.
72
     *
73
     * @param   Store           $store
74
     * @param   AbstractModel[] $models
75
     */
76
    public function __construct(Store $store, array $models = [])
77
    {
78
        $this->pos = 0;
79
        $this->store = $store;
80
        $this->setModels($models);
81
    }
82
83
    /**
84
     * Calculates the change set of this collection.
85
     *
86
     * @return  array
87
     */
88
    public function calculateChangeSet()
89
    {
90
        if (false === $this->isDirty()) {
91
            return [];
92
        }
93
        return [
94
            'old' => empty($this->original) ? null : $this->original,
95
            'new' => empty($this->models) ? null : $this->models,
96
        ];
97
    }
98
99
    /**
100
     * Clears/empties the collection.
101
     *
102
     * @return  self
103
     */
104
    public function clear()
105
    {
106
        $this->models = [];
107
        $this->added = [];
108
        $this->removed = $this->original;
109
        return $this;
110
    }
111
112
    /**
113
     * {@inheritDoc}
114
     */
115
    public function count()
116
    {
117
        return count($this->models);
118
    }
119
120
    /**
121
     * {@inheritDoc}
122
     */
123
    public function current()
124
    {
125
        return $this->models[$this->pos];
126
    }
127
128
    /**
129
     * Gets a single model result from the collection.
130
     *
131
     * @return  AbstractModel|null
132
     */
133
    public function getSingleResult()
134
    {
135
        if (0 === $this->count()) {
136
            return null;
137
        }
138
        $this->rewind();
139
        return $this->current();
140
    }
141
142
    /**
143
     * Gets the model collection type.
144
     *
145
     * @return  string
146
     */
147
    abstract public function getType();
148
149
    /**
150
     * Determines if the Model is included in the collection.
151
     *
152
     * @param   AbstractModel   $model  The model to check.
153
     * @return  bool
154
     */
155
    public function has(AbstractModel $model)
156
    {
157
        return -1 !== $this->indexOf('models', $model);
158
    }
159
160
    /**
161
     * Determines if any models in this collection are dirty (have changes).
162
     *
163
     * @return  bool
164
     */
165
    public function hasDirtyModels()
166
    {
167
        foreach ($this->models as $model) {
168
            if (true === $model->isDirty()) {
169
                return true;
170
            }
171
        }
172
        return false;
173
    }
174
175
    /**
176
     * Determines if the collection is dirty.
177
     *
178
     * @return  bool
179
     */
180
    public function isDirty()
181
    {
182
        return !empty($this->added) || !empty($this->removed);
183
    }
184
185
    /**
186
     * Determines if this collection is empty.
187
     *
188
     * @return  bool
189
     */
190
    public function isEmpty()
191
    {
192
        return 0 === $this->count();
193
    }
194
195
    /**
196
     * Determines if models in this collection have been loaded from the persistence layer.
197
     *
198
     * @return  bool
199
     */
200
    public function isLoaded()
201
    {
202
        return $this->loaded;
203
    }
204
205
    /**
206
     * {@inheritDoc}
207
     */
208
    public function key()
209
    {
210
        return $this->pos;
211
    }
212
213
    /**
214
     * {@inheritDoc}
215
     */
216
    public function next()
217
    {
218
        ++$this->pos;
219
    }
220
221
    /**
222
     * Pushes a Model into the collection.
223
     *
224
     * @param   AbstractModel   $model  The model to push.
225
     * @return  self
226
     */
227 View Code Duplication
    public function push(AbstractModel $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...
228
    {
229
        $this->validateAdd($model);
230
        if (true === $this->willAdd($model)) {
231
            return $this;
232
        }
233
        if (true === $this->willRemove($model)) {
234
            $this->evict('removed', $model);
235
            $this->set('models', $model);
236
            return $this;
237
        }
238
        if (true === $this->hasOriginal($model)) {
239
            return $this;
240
        }
241
        $this->set('added', $model);
242
        $this->set('models', $model);
243
        return $this;
244
    }
245
246
    /**
247
     * Removes a model from the collection.
248
     *
249
     * @param   AbstractModel   $model  The model to remove.
250
     * @return  self
251
     */
252 View Code Duplication
    public function remove(AbstractModel $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...
253
    {
254
        $this->validateAdd($model);
255
        if (true === $this->willRemove($model)) {
256
            return $this;
257
        }
258
259
        if (true === $this->willAdd($model)) {
260
            $this->evict('added', $model);
261
            $this->evict('models', $model);
262
            return $this;
263
        }
264
265
        if (true === $this->hasOriginal($model)) {
266
            $this->evict('models', $model);
267
            $this->set('removed', $model);
268
        }
269
        return $this;
270
    }
271
272
    /**
273
     * {@inheritDoc}
274
     */
275
    public function rewind()
276
    {
277
        $this->pos = 0;
278
    }
279
280
    /**
281
     * Rollsback the collection it it's original state.
282
     *
283
     * @return  self
284
     */
285
    public function rollback()
286
    {
287
        $this->models = $this->original;
288
        $this->added = [];
289
        $this->removed = [];
290
        return $this;
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296
    public function valid()
297
    {
298
        return isset($this->models[$this->pos]);
299
    }
300
301
    /**
302
     * Determines if the model is scheduled for addition to the collection.
303
     *
304
     * @param   AbstractModel   $model  The model to check.
305
     * @return  bool
306
     */
307
    public function willAdd(AbstractModel $model)
308
    {
309
        return -1 !== $this->indexOf('added', $model);
310
    }
311
312
    /**
313
     * Determines if the model is scheduled for removal from the collection.
314
     *
315
     * @param   AbstractModel   $model  The model to check.
316
     * @return  bool
317
     */
318
    public function willRemove(AbstractModel $model)
319
    {
320
        return -1 !== $this->indexOf('removed', $model);
321
    }
322
323
    /**
324
     * Adds an model to this collection.
325
     * Is used during initial collection construction.
326
     *
327
     * @param   AbstractModel   $model
328
     * @return  self
329
     */
330
    protected function add(AbstractModel $model)
331
    {
332
        if (true === $this->has($model)) {
333
            return $this;
334
        }
335
        $this->validateAdd($model);
336
        if (true === $model->getState()->is('empty')) {
337
            $this->loaded = false;
338
        }
339
        $this->models[] = $model;
340
        if (false === $this->hasOriginal($model)) {
341
            $this->original[] = $model;
342
        }
343
        return $this;
344
    }
345
346
    /**
347
     * Evicts a model from a collection property (original, added, removed, models).
348
     *
349
     * @param   string          $property   The property key
350
     * @param   AbstractModel   $model      The model to set.
351
     * @return  self
352
     */
353
    protected function evict($property, AbstractModel $model)
354
    {
355
        $index = $this->indexOf($property, $model);
356
        $models = $this->$property;
357
        unset($models[$index]);
358
        $this->$property = array_values($models);
359
        return $this;
360
    }
361
362
    /**
363
     * Determines if the model is included in the original set.
364
     *
365
     * @param   AbstractModel   $model  The model to check.
366
     * @return  bool
367
     */
368
    protected function hasOriginal(AbstractModel $model)
369
    {
370
        return -1 !== $this->indexOf('original', $model);
371
    }
372
373
    /**
374
     * Gets the Model array index from a collection property (original, added, removed, models).
375
     * Will return -1 if the model was not found.
376
     *
377
     * @param   string          $property   The property key
378
     * @param   AbstractModel   $model      The model to check.
379
     * @return  int
380
     */
381
    protected function indexOf($property, AbstractModel $model)
382
    {
383
        $this->validateModelClass($model);
384
385
        // @todo For performance, can we create a map using the model's composite key to avoid these loops?
386
        foreach ($this->$property as $index => $loaded) {
387
            if (true === $this->modelsMatch($model, $loaded)) {
388
                return $index;
389
            }
390
        }
391
        return -1;
392
    }
393
394
    /**
395
     * Determines if the provided models match.
396
     *
397
     * @param   AbstractModel   $model
398
     * @param   AbstractModel   $loaded
399
     * @return  bool
400
     */
401
    abstract protected function modelsMatch(AbstractModel $model, AbstractModel $loaded);
402
403
    /**
404
     * Sets a model to a collection property (original, added, removed, models).
405
     *
406
     * @param   string          $property   The property key
407
     * @param   AbstractModel   $model      The model to set.
408
     * @return  self
409
     */
410
    protected function set($property, AbstractModel $model)
411
    {
412
        $models = $this->$property;
413
        $models[] = $model;
414
        $this->$property = $models;
415
        return $this;
416
    }
417
418
    /**
419
     * Sets an array of models to the collection.
420
     *
421
     * @param   AbstractModel[]     $models
422
     * @return  self
423
     */
424
    protected function setModels(array $models)
425
    {
426
        foreach ($models as $model) {
427
            $this->add($model);
428
        }
429
        return $this;
430
    }
431
432
    /**
433
     * Validates that the collection supports the incoming model.
434
     *
435
     * @param   AbstractModel   $model  The model to validate.
436
     * @throws  \InvalidArgumentException
437
     */
438
    abstract protected function validateAdd(AbstractModel $model);
439
}
440