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) |
|
|
|
|
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) |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.