Completed
Push — master ( dd6356...9040ae )
by Jacob
9s
created

AbstractCollection::getTotalCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
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
     * The total count of the collection,
79
     * Acts as if no offsets or limits were originally applied to the Model set.
80
     *
81
     * @var int
82
     */
83
    protected $totalCount;
84
85
    /**
86
     * Constructor.
87
     *
88
     * @param   Store           $store
89
     * @param   AbstractModel[] $models
90
     * @param   int             $totalCount
91
     */
92
    public function __construct(Store $store, array $models = [], $totalCount)
93
    {
94
        $this->pos = 0;
95
        $this->store = $store;
96
        $this->setModels($models);
97
        $this->totalCount = (Integer) $totalCount;
98
    }
99
100
    /**
101
     * Calculates the change set of this collection.
102
     *
103
     * @return  array
104
     */
105
    public function calculateChangeSet()
106
    {
107
        if (false === $this->isDirty()) {
108
            return [];
109
        }
110
        return [
111
            'old' => empty($this->original) ? null : $this->original,
112
            'new' => empty($this->models) ? null : $this->models,
113
        ];
114
    }
115
116
    /**
117
     * Clears/empties the collection.
118
     *
119
     * @return  self
120
     */
121
    public function clear()
122
    {
123
        $this->models = [];
124
        $this->added = [];
125
        $this->removed = $this->original;
126
        return $this;
127
    }
128
129
    /**
130
     * {@inheritDoc}
131
     */
132
    public function count()
133
    {
134
        return count($this->models);
135
    }
136
137
    /**
138
     * {@inheritDoc}
139
     */
140
    public function current()
141
    {
142
        $key = $this->modelKeyMap[$this->pos];
143
        return $this->models[$key];
144
    }
145
146
    /**
147
     * Gets a single model result from the collection.
148
     *
149
     * @return  AbstractModel|null
150
     */
151
    public function getSingleResult()
152
    {
153
        if (0 === $this->count()) {
154
            return null;
155
        }
156
        $this->rewind();
157
        return $this->current();
158
    }
159
160
    /**
161
     * Gets the 'total' model count, as if a limit and offset were not applied.
162
     *
163
     * @return  int
164
     */
165
    public function getTotalCount()
166
    {
167
        return $this->totalCount;
168
    }
169
170
    /**
171
     * Gets the model collection type.
172
     *
173
     * @return  string
174
     */
175
    abstract public function getType();
176
177
    /**
178
     * Determines if the Model is included in the collection.
179
     *
180
     * @param   AbstractModel   $model  The model to check.
181
     * @return  bool
182
     */
183
    public function has(AbstractModel $model)
184
    {
185
        $key = $model->getCompositeKey();
186
        return isset($this->models[$key]);
187
    }
188
189
    /**
190
     * Determines if any models in this collection are dirty (have changes).
191
     *
192
     * @return  bool
193
     */
194
    public function hasDirtyModels()
195
    {
196
        foreach ($this->models as $model) {
197
            if (true === $model->isDirty()) {
198
                return true;
199
            }
200
        }
201
        return false;
202
    }
203
204
    /**
205
     * Determines if the collection is dirty.
206
     *
207
     * @return  bool
208
     */
209
    public function isDirty()
210
    {
211
        return !empty($this->added) || !empty($this->removed);
212
    }
213
214
    /**
215
     * Determines if this collection is empty.
216
     *
217
     * @return  bool
218
     */
219
    public function isEmpty()
220
    {
221
        return 0 === $this->count();
222
    }
223
224
    /**
225
     * Determines if models in this collection have been loaded from the persistence layer.
226
     *
227
     * @return  bool
228
     */
229
    public function isLoaded()
230
    {
231
        return $this->loaded;
232
    }
233
234
    /**
235
     * {@inheritDoc}
236
     */
237
    public function key()
238
    {
239
        return $this->pos;
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245
    public function next()
246
    {
247
        ++$this->pos;
248
    }
249
250
    /**
251
     * Pushes a Model into the collection.
252
     *
253
     * @param   AbstractModel   $model  The model to push.
254
     * @return  self
255
     */
256 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...
257
    {
258
        $this->validateAdd($model);
259
        if (true === $this->willAdd($model)) {
260
            return $this;
261
        }
262
        if (true === $this->willRemove($model)) {
263
            $this->evict('removed', $model);
264
            $this->set('models', $model);
265
            return $this;
266
        }
267
        if (true === $this->hasOriginal($model)) {
268
            return $this;
269
        }
270
        $this->set('added', $model);
271
        $this->set('models', $model);
272
        return $this;
273
    }
274
275
    /**
276
     * Removes a model from the collection.
277
     *
278
     * @param   AbstractModel   $model  The model to remove.
279
     * @return  self
280
     */
281 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...
282
    {
283
        $this->validateAdd($model);
284
        if (true === $this->willRemove($model)) {
285
            return $this;
286
        }
287
288
        if (true === $this->willAdd($model)) {
289
            $this->evict('added', $model);
290
            $this->evict('models', $model);
291
            return $this;
292
        }
293
294
        if (true === $this->hasOriginal($model)) {
295
            $this->evict('models', $model);
296
            $this->set('removed', $model);
297
        }
298
        return $this;
299
    }
300
301
    /**
302
     * {@inheritDoc}
303
     */
304
    public function rewind()
305
    {
306
        $this->pos = 0;
307
    }
308
309
    /**
310
     * Rollsback the collection it it's original state.
311
     *
312
     * @return  self
313
     */
314
    public function rollback()
315
    {
316
        $this->models = $this->original;
317
        $this->added = [];
318
        $this->removed = [];
319
        return $this;
320
    }
321
322
    /**
323
     * {@inheritDoc}
324
     */
325
    public function valid()
326
    {
327
        return isset($this->modelKeyMap[$this->pos]);
328
    }
329
330
    /**
331
     * Determines if the model is scheduled for addition to the collection.
332
     *
333
     * @param   AbstractModel   $model  The model to check.
334
     * @return  bool
335
     */
336
    public function willAdd(AbstractModel $model)
337
    {
338
        $key = $model->getCompositeKey();
339
        return isset($this->added[$key]);
340
    }
341
342
    /**
343
     * Determines if the model is scheduled for removal from the collection.
344
     *
345
     * @param   AbstractModel   $model  The model to check.
346
     * @return  bool
347
     */
348
    public function willRemove(AbstractModel $model)
349
    {
350
        $key = $model->getCompositeKey();
351
        return isset($this->removed[$key]);
352
    }
353
354
    /**
355
     * Adds an model to this collection.
356
     * Is used during initial collection construction.
357
     *
358
     * @param   AbstractModel   $model
359
     * @return  self
360
     */
361
    protected function add(AbstractModel $model)
362
    {
363
        if (true === $this->has($model)) {
364
            return $this;
365
        }
366
        $this->validateAdd($model);
367
        if (true === $model->getState()->is('empty')) {
368
            $this->loaded = false;
369
        }
370
371
        $key = $model->getCompositeKey();
372
        $this->models[$key] = $model;
373
        $this->modelKeyMap[] = $key;
374
375
        if (false === $this->hasOriginal($model)) {
376
            $this->original[$key] = $model;
377
        }
378
        return $this;
379
    }
380
381
    /**
382
     * Evicts a model from a collection property (original, added, removed, models).
383
     *
384
     * @param   string          $property   The property key
385
     * @param   AbstractModel   $model      The model to set.
386
     * @return  self
387
     */
388
    protected function evict($property, AbstractModel $model)
389
    {
390
        $key = $model->getCompositeKey();
391
        if (isset($this->$property)) {
392
            unset($this->$property[$key]);
393
        }
394
395
        if ('models' === $property) {
396
            $keys = array_flip($this->modelKeyMap);
397
            if (isset($keys[$key])) {
398
                unset($keys[$key]);
399
                $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...
400
                $this->totalCount--;
401
            }
402
        }
403
        return $this;
404
    }
405
406
    /**
407
     * Determines if the model is included in the original set.
408
     *
409
     * @param   AbstractModel   $model  The model to check.
410
     * @return  bool
411
     */
412
    protected function hasOriginal(AbstractModel $model)
413
    {
414
        $key = $model->getCompositeKey();
415
        return isset($this->original[$key]);
416
    }
417
418
    /**
419
     * Sets a model to a collection property (original, added, removed, models).
420
     *
421
     * @param   string          $property   The property key
422
     * @param   AbstractModel   $model      The model to set.
423
     * @return  self
424
     */
425
    protected function set($property, AbstractModel $model)
426
    {
427
        $key = $model->getCompositeKey();
428
        $this->$property[$key] = $model;
429
430
        if ('models' === $property) {
431
            $keys = array_flip($this->models);
432
            if (!isset($keys[$key])) {
433
                $this->modelKeyMap[] = $key;
434
                $this->totalCount++;
435
            }
436
        }
437
        return $this;
438
    }
439
440
    /**
441
     * Sets an array of models to the collection.
442
     *
443
     * @param   AbstractModel[]     $models
444
     * @return  self
445
     */
446
    protected function setModels(array $models)
447
    {
448
        foreach ($models as $model) {
449
            $this->add($model);
450
        }
451
        return $this;
452
    }
453
454
    /**
455
     * Validates that the collection supports the incoming model.
456
     *
457
     * @param   AbstractModel   $model  The model to validate.
458
     * @throws  \InvalidArgumentException
459
     */
460
    abstract protected function validateAdd(AbstractModel $model);
461
}
462