Completed
Push — master ( ec89fd...ab8bf4 )
by Jacob
04:45 queued 01:47
created

AbstractCollection::indexOf()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
rs 9.4285
cc 3
eloc 6
nc 3
nop 2
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
     * Gets the position of each model key in the collection.
43
     *
44
     * @var string[]
45
     */
46
    protected $modelKeyMap = [];
47
48
    /**
49
     * Original models assigned to this collection.
50
     *
51
     * @var AbstractModel[]
52
     */
53
    protected $original = [];
54
55
    /**
56
     * The array position.
57
     *
58
     * @var int
59
     */
60
    protected $pos = 0;
61
62
    /**
63
     * Models removed from this collection.
64
     * Tracks removed models for rollback/change purposes.
65
     *
66
     * @var AbstractModel[]
67
     */
68
    protected $removed = [];
69
70
    /**
71
     * The store for handling storage operations.
72
     *
73
     * @var Store
74
     */
75
    protected $store;
76
77
    /**
78
     * Constructor.
79
     *
80
     * @param   Store           $store
81
     * @param   AbstractModel[] $models
82
     */
83
    public function __construct(Store $store, array $models = [])
84
    {
85
        $this->pos = 0;
86
        $this->store = $store;
87
        $this->setModels($models);
88
    }
89
90
    /**
91
     * Calculates the change set of this collection.
92
     *
93
     * @return  array
94
     */
95
    public function calculateChangeSet()
96
    {
97
        if (false === $this->isDirty()) {
98
            return [];
99
        }
100
        return [
101
            'old' => empty($this->original) ? null : $this->original,
102
            'new' => empty($this->models) ? null : $this->models,
103
        ];
104
    }
105
106
    /**
107
     * Clears/empties the collection.
108
     *
109
     * @return  self
110
     */
111
    public function clear()
112
    {
113
        $this->models = [];
114
        $this->added = [];
115
        $this->removed = $this->original;
116
        return $this;
117
    }
118
119
    /**
120
     * {@inheritDoc}
121
     */
122
    public function count()
123
    {
124
        return count($this->models);
125
    }
126
127
    /**
128
     * {@inheritDoc}
129
     */
130
    public function current()
131
    {
132
        $key = $this->modelKeyMap[$this->pos];
133
        return $this->models[$key];
134
    }
135
136
    /**
137
     * Gets a single model result from the collection.
138
     *
139
     * @return  AbstractModel|null
140
     */
141
    public function getSingleResult()
142
    {
143
        if (0 === $this->count()) {
144
            return null;
145
        }
146
        $this->rewind();
147
        return $this->current();
148
    }
149
150
    /**
151
     * Gets the model collection type.
152
     *
153
     * @return  string
154
     */
155
    abstract public function getType();
156
157
    /**
158
     * Determines if the Model is included in the collection.
159
     *
160
     * @param   AbstractModel   $model  The model to check.
161
     * @return  bool
162
     */
163
    public function has(AbstractModel $model)
164
    {
165
        $key = $model->getCompositeKey();
166
        return isset($this->models[$key]);
167
    }
168
169
    /**
170
     * Determines if any models in this collection are dirty (have changes).
171
     *
172
     * @return  bool
173
     */
174
    public function hasDirtyModels()
175
    {
176
        foreach ($this->models as $model) {
177
            if (true === $model->isDirty()) {
178
                return true;
179
            }
180
        }
181
        return false;
182
    }
183
184
    /**
185
     * Determines if the collection is dirty.
186
     *
187
     * @return  bool
188
     */
189
    public function isDirty()
190
    {
191
        return !empty($this->added) || !empty($this->removed);
192
    }
193
194
    /**
195
     * Determines if this collection is empty.
196
     *
197
     * @return  bool
198
     */
199
    public function isEmpty()
200
    {
201
        return 0 === $this->count();
202
    }
203
204
    /**
205
     * Determines if models in this collection have been loaded from the persistence layer.
206
     *
207
     * @return  bool
208
     */
209
    public function isLoaded()
210
    {
211
        return $this->loaded;
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217
    public function key()
218
    {
219
        return $this->pos;
220
    }
221
222
    /**
223
     * {@inheritDoc}
224
     */
225
    public function next()
226
    {
227
        ++$this->pos;
228
    }
229
230
    /**
231
     * Pushes a Model into the collection.
232
     *
233
     * @param   AbstractModel   $model  The model to push.
234
     * @return  self
235
     */
236 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...
237
    {
238
        $this->validateAdd($model);
239
        if (true === $this->willAdd($model)) {
240
            return $this;
241
        }
242
        if (true === $this->willRemove($model)) {
243
            $this->evict('removed', $model);
244
            $this->set('models', $model);
245
            return $this;
246
        }
247
        if (true === $this->hasOriginal($model)) {
248
            return $this;
249
        }
250
        $this->set('added', $model);
251
        $this->set('models', $model);
252
        return $this;
253
    }
254
255
    /**
256
     * Removes a model from the collection.
257
     *
258
     * @param   AbstractModel   $model  The model to remove.
259
     * @return  self
260
     */
261 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...
262
    {
263
        $this->validateAdd($model);
264
        if (true === $this->willRemove($model)) {
265
            return $this;
266
        }
267
268
        if (true === $this->willAdd($model)) {
269
            $this->evict('added', $model);
270
            $this->evict('models', $model);
271
            return $this;
272
        }
273
274
        if (true === $this->hasOriginal($model)) {
275
            $this->evict('models', $model);
276
            $this->set('removed', $model);
277
        }
278
        return $this;
279
    }
280
281
    /**
282
     * {@inheritDoc}
283
     */
284
    public function rewind()
285
    {
286
        $this->pos = 0;
287
    }
288
289
    /**
290
     * Rollsback the collection it it's original state.
291
     *
292
     * @return  self
293
     */
294
    public function rollback()
295
    {
296
        $this->models = $this->original;
297
        $this->added = [];
298
        $this->removed = [];
299
        return $this;
300
    }
301
302
    /**
303
     * {@inheritDoc}
304
     */
305
    public function valid()
306
    {
307
        return isset($this->modelKeyMap[$this->pos]);
308
    }
309
310
    /**
311
     * Determines if the model is scheduled for addition to the collection.
312
     *
313
     * @param   AbstractModel   $model  The model to check.
314
     * @return  bool
315
     */
316
    public function willAdd(AbstractModel $model)
317
    {
318
        $key = $model->getCompositeKey();
319
        return isset($this->added[$key]);
320
    }
321
322
    /**
323
     * Determines if the model is scheduled for removal from the collection.
324
     *
325
     * @param   AbstractModel   $model  The model to check.
326
     * @return  bool
327
     */
328
    public function willRemove(AbstractModel $model)
329
    {
330
        $key = $model->getCompositeKey();
331
        return isset($this->removed[$key]);
332
    }
333
334
    /**
335
     * Adds an model to this collection.
336
     * Is used during initial collection construction.
337
     *
338
     * @param   AbstractModel   $model
339
     * @return  self
340
     */
341
    protected function add(AbstractModel $model)
342
    {
343
        if (true === $this->has($model)) {
344
            return $this;
345
        }
346
        $this->validateAdd($model);
347
        if (true === $model->getState()->is('empty')) {
348
            $this->loaded = false;
349
        }
350
351
        $key = $model->getCompositeKey();
352
        $this->models[$key] = $model;
353
        $this->modelKeyMap[] = $key;
354
355
        if (false === $this->hasOriginal($model)) {
356
            $this->original[$key] = $model;
357
        }
358
        return $this;
359
    }
360
361
    /**
362
     * Evicts a model from a collection property (original, added, removed, models).
363
     *
364
     * @param   string          $property   The property key
365
     * @param   AbstractModel   $model      The model to set.
366
     * @return  self
367
     */
368
    protected function evict($property, AbstractModel $model)
369
    {
370
        $key = $model->getCompositeKey();
371
        if (isset($this->$property)) {
372
            unset($this->$property[$key]);
373
        }
374
375 View Code Duplication
        if ('models' === $property) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
376
            $keys = array_flip($this->modelKeyMap);
377
            if (isset($keys[$key])) {
378
                unset($keys[$key]);
379
                $this->modelKeyMap = array_keys($keys);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_keys($keys) of type array<integer,integer|string> is incompatible with the declared type array<integer,string> of property $modelKeyMap.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
380
            }
381
        }
382
        return $this;
383
    }
384
385
    /**
386
     * Determines if the model is included in the original set.
387
     *
388
     * @param   AbstractModel   $model  The model to check.
389
     * @return  bool
390
     */
391
    protected function hasOriginal(AbstractModel $model)
392
    {
393
        $key = $model->getCompositeKey();
394
        return isset($this->original[$key]);
395
    }
396
397
    /**
398
     * Sets a model to a collection property (original, added, removed, models).
399
     *
400
     * @param   string          $property   The property key
401
     * @param   AbstractModel   $model      The model to set.
402
     * @return  self
403
     */
404
    protected function set($property, AbstractModel $model)
405
    {
406
        $key = $model->getCompositeKey();
407
        $this->$property[$key] = $model;
408
409 View Code Duplication
        if ('models' === $property) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
410
            $keys = array_flip($this->models);
411
            if (!isset($keys[$key])) {
412
                $this->modelKeyMap[] = $key;
413
            }
414
        }
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