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