1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace EventEspresso\core\services\collections; |
4
|
|
|
|
5
|
|
|
use EventEspresso\core\exceptions\InvalidEntityException; |
6
|
|
|
use EventEspresso\core\exceptions\InvalidInterfaceException; |
7
|
|
|
use LimitIterator; |
8
|
|
|
use SplObjectStorage; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Class Collection |
12
|
|
|
* class for managing a set of entities that all adhere to the same interface |
13
|
|
|
* unofficially follows Interop\Container\ContainerInterface |
14
|
|
|
* |
15
|
|
|
* @package Event Espresso |
16
|
|
|
* @author Brent Christensen |
17
|
|
|
* @since 4.9.0 |
18
|
|
|
*/ |
19
|
|
|
class Collection extends SplObjectStorage implements CollectionInterface |
20
|
|
|
{ |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* a unique string for identifying this collection |
24
|
|
|
* |
25
|
|
|
* @type string $collection_identifier |
26
|
|
|
*/ |
27
|
|
|
protected $collection_identifier; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* an interface (or class) name to be used for restricting the type of objects added to the storage |
31
|
|
|
* this should be set from within the child class constructor |
32
|
|
|
* |
33
|
|
|
* @type string $interface |
34
|
|
|
*/ |
35
|
|
|
protected $collection_interface; |
36
|
|
|
|
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Collection constructor |
40
|
|
|
* |
41
|
|
|
* @param string $collection_interface |
42
|
|
|
* @throws InvalidInterfaceException |
43
|
|
|
*/ |
44
|
|
|
public function __construct($collection_interface) |
45
|
|
|
{ |
46
|
|
|
$this->setCollectionInterface($collection_interface); |
47
|
|
|
$this->setCollectionIdentifier(); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @return string |
53
|
|
|
*/ |
54
|
|
|
public function collectionIdentifier() |
55
|
|
|
{ |
56
|
|
|
return $this->collection_identifier; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* creates a very readable unique 9 character identifier like: CF2-532-DAC |
62
|
|
|
* and appends it to the non-qualified class name, ex: ThingCollection-CF2-532-DAC |
63
|
|
|
* |
64
|
|
|
* @return void |
65
|
|
|
*/ |
66
|
|
|
protected function setCollectionIdentifier() |
67
|
|
|
{ |
68
|
|
|
// hash a few collection details |
69
|
|
|
$identifier = md5(spl_object_hash($this) . $this->collection_interface . time()); |
70
|
|
|
// grab a few characters from the start, middle, and end of the hash |
71
|
|
|
$id = array(); |
72
|
|
|
for ($x = 0; $x < 19; $x += 9) { |
73
|
|
|
$id[] = substr($identifier, $x, 3); |
74
|
|
|
} |
75
|
|
|
$identifier = basename(str_replace('\\', '/', get_class($this))); |
76
|
|
|
$identifier .= '-' . strtoupper(implode('-', $id)); |
77
|
|
|
$this->collection_identifier = $identifier; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* setCollectionInterface |
83
|
|
|
* |
84
|
|
|
* @access protected |
85
|
|
|
* @param string $collection_interface |
86
|
|
|
* @throws \EventEspresso\core\exceptions\InvalidInterfaceException |
87
|
|
|
*/ |
88
|
|
View Code Duplication |
protected function setCollectionInterface($collection_interface) |
89
|
|
|
{ |
90
|
|
|
if (! (interface_exists($collection_interface) || class_exists($collection_interface))) { |
91
|
|
|
throw new InvalidInterfaceException($collection_interface); |
92
|
|
|
} |
93
|
|
|
$this->collection_interface = $collection_interface; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* add |
99
|
|
|
* attaches an object to the Collection |
100
|
|
|
* and sets any supplied data associated with the current iterator entry |
101
|
|
|
* by calling EE_Object_Collection::set_identifier() |
102
|
|
|
* |
103
|
|
|
* @access public |
104
|
|
|
* @param $object |
105
|
|
|
* @param mixed $identifier |
106
|
|
|
* @return bool |
107
|
|
|
* @throws InvalidEntityException |
108
|
|
|
* @throws DuplicateCollectionIdentifierException |
109
|
|
|
*/ |
110
|
|
|
public function add($object, $identifier = null) |
111
|
|
|
{ |
112
|
|
|
if (! $object instanceof $this->collection_interface) { |
113
|
|
|
throw new InvalidEntityException($object, $this->collection_interface); |
114
|
|
|
} |
115
|
|
|
if ($this->contains($object)) { |
116
|
|
|
throw new DuplicateCollectionIdentifierException($identifier); |
117
|
|
|
} |
118
|
|
|
$this->attach($object); |
119
|
|
|
$this->setIdentifier($object, $identifier); |
120
|
|
|
return $this->contains($object); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* setIdentifier |
126
|
|
|
* Sets the data associated with an object in the Collection |
127
|
|
|
* if no $identifier is supplied, then the spl_object_hash() is used |
128
|
|
|
* |
129
|
|
|
* @access public |
130
|
|
|
* @param $object |
131
|
|
|
* @param mixed $identifier |
132
|
|
|
* @return bool |
133
|
|
|
*/ |
134
|
|
View Code Duplication |
public function setIdentifier($object, $identifier = null) |
135
|
|
|
{ |
136
|
|
|
$identifier = ! empty($identifier) |
137
|
|
|
? $identifier |
138
|
|
|
: spl_object_hash($object); |
139
|
|
|
$this->rewind(); |
140
|
|
|
while ($this->valid()) { |
141
|
|
|
if ($object === $this->current()) { |
142
|
|
|
$this->setInfo($identifier); |
143
|
|
|
$this->rewind(); |
144
|
|
|
return true; |
145
|
|
|
} |
146
|
|
|
$this->next(); |
147
|
|
|
} |
148
|
|
|
return false; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* get |
154
|
|
|
* finds and returns an object in the Collection based on the identifier that was set using addObject() |
155
|
|
|
* PLZ NOTE: the pointer is reset to the beginning of the collection before returning |
156
|
|
|
* |
157
|
|
|
* @access public |
158
|
|
|
* @param mixed $identifier |
159
|
|
|
* @return mixed |
160
|
|
|
*/ |
161
|
|
View Code Duplication |
public function get($identifier) |
162
|
|
|
{ |
163
|
|
|
$this->rewind(); |
164
|
|
|
while ($this->valid()) { |
165
|
|
|
if ($identifier === $this->getInfo()) { |
166
|
|
|
$object = $this->current(); |
167
|
|
|
$this->rewind(); |
168
|
|
|
return $object; |
169
|
|
|
} |
170
|
|
|
$this->next(); |
171
|
|
|
} |
172
|
|
|
return null; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* has |
178
|
|
|
* returns TRUE or FALSE |
179
|
|
|
* depending on whether the object is within the Collection |
180
|
|
|
* based on the supplied $identifier |
181
|
|
|
* |
182
|
|
|
* @access public |
183
|
|
|
* @param mixed $identifier |
184
|
|
|
* @return bool |
185
|
|
|
*/ |
186
|
|
View Code Duplication |
public function has($identifier) |
187
|
|
|
{ |
188
|
|
|
$this->rewind(); |
189
|
|
|
while ($this->valid()) { |
190
|
|
|
if ($identifier === $this->getInfo()) { |
191
|
|
|
$this->rewind(); |
192
|
|
|
return true; |
193
|
|
|
} |
194
|
|
|
$this->next(); |
195
|
|
|
} |
196
|
|
|
return false; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* hasObject |
202
|
|
|
* returns TRUE or FALSE depending on whether the supplied object is within the Collection |
203
|
|
|
* |
204
|
|
|
* @access public |
205
|
|
|
* @param $object |
206
|
|
|
* @return bool |
207
|
|
|
*/ |
208
|
|
|
public function hasObject($object) |
209
|
|
|
{ |
210
|
|
|
return $this->contains($object); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* hasObjects |
216
|
|
|
* returns true if there are objects within the Collection, and false if it is empty |
217
|
|
|
* |
218
|
|
|
* @access public |
219
|
|
|
* @return bool |
220
|
|
|
*/ |
221
|
|
|
public function hasObjects() |
222
|
|
|
{ |
223
|
|
|
return $this->count() !== 0; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* isEmpty |
229
|
|
|
* returns true if there are no objects within the Collection, and false if there are |
230
|
|
|
* |
231
|
|
|
* @access public |
232
|
|
|
* @return bool |
233
|
|
|
*/ |
234
|
|
|
public function isEmpty() |
235
|
|
|
{ |
236
|
|
|
return $this->count() === 0; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* remove |
242
|
|
|
* detaches an object from the Collection |
243
|
|
|
* |
244
|
|
|
* @access public |
245
|
|
|
* @param $object |
246
|
|
|
* @return bool |
247
|
|
|
*/ |
248
|
|
|
public function remove($object) |
249
|
|
|
{ |
250
|
|
|
$this->detach($object); |
251
|
|
|
return true; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* setCurrent |
257
|
|
|
* advances pointer to the object whose identifier matches that which was provided |
258
|
|
|
* |
259
|
|
|
* @access public |
260
|
|
|
* @param mixed $identifier |
261
|
|
|
* @return boolean |
262
|
|
|
*/ |
263
|
|
View Code Duplication |
public function setCurrent($identifier) |
264
|
|
|
{ |
265
|
|
|
$this->rewind(); |
266
|
|
|
while ($this->valid()) { |
267
|
|
|
if ($identifier === $this->getInfo()) { |
268
|
|
|
return true; |
269
|
|
|
} |
270
|
|
|
$this->next(); |
271
|
|
|
} |
272
|
|
|
return false; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* setCurrentUsingObject |
278
|
|
|
* advances pointer to the provided object |
279
|
|
|
* |
280
|
|
|
* @access public |
281
|
|
|
* @param $object |
282
|
|
|
* @return boolean |
283
|
|
|
*/ |
284
|
|
|
public function setCurrentUsingObject($object) |
285
|
|
|
{ |
286
|
|
|
$this->rewind(); |
287
|
|
|
while ($this->valid()) { |
288
|
|
|
if ($this->current() === $object) { |
289
|
|
|
return true; |
290
|
|
|
} |
291
|
|
|
$this->next(); |
292
|
|
|
} |
293
|
|
|
return false; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Returns the object occupying the index before the current object, |
299
|
|
|
* unless this is already the first object, in which case it just returns the first object |
300
|
|
|
* |
301
|
|
|
* @return mixed |
302
|
|
|
*/ |
303
|
|
|
public function previous() |
304
|
|
|
{ |
305
|
|
|
$index = $this->indexOf($this->current()); |
306
|
|
|
if ($index === 0) { |
307
|
|
|
return $this->current(); |
308
|
|
|
} |
309
|
|
|
$index--; |
310
|
|
|
return $this->objectAtIndex($index); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Returns the index of a given object, or false if not found |
316
|
|
|
* |
317
|
|
|
* @see http://stackoverflow.com/a/8736013 |
318
|
|
|
* @param $object |
319
|
|
|
* @return boolean|int|string |
320
|
|
|
*/ |
321
|
|
|
public function indexOf($object) |
322
|
|
|
{ |
323
|
|
|
if (! $this->contains($object)) { |
324
|
|
|
return false; |
325
|
|
|
} |
326
|
|
|
foreach ($this as $index => $obj) { |
327
|
|
|
if ($obj === $object) { |
328
|
|
|
return $index; |
329
|
|
|
} |
330
|
|
|
} |
331
|
|
|
return false; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Returns the object at the given index |
337
|
|
|
* |
338
|
|
|
* @see http://stackoverflow.com/a/8736013 |
339
|
|
|
* @param int $index |
340
|
|
|
* @return mixed |
341
|
|
|
*/ |
342
|
|
|
public function objectAtIndex($index) |
343
|
|
|
{ |
344
|
|
|
$iterator = new LimitIterator($this, $index, 1); |
345
|
|
|
$iterator->rewind(); |
346
|
|
|
return $iterator->current(); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Returns the sequence of objects as specified by the offset and length |
352
|
|
|
* |
353
|
|
|
* @see http://stackoverflow.com/a/8736013 |
354
|
|
|
* @param int $offset |
355
|
|
|
* @param int $length |
356
|
|
|
* @return array |
357
|
|
|
*/ |
358
|
|
|
public function slice($offset, $length) |
359
|
|
|
{ |
360
|
|
|
$slice = array(); |
361
|
|
|
$iterator = new LimitIterator($this, $offset, $length); |
362
|
|
|
foreach ($iterator as $object) { |
363
|
|
|
$slice[] = $object; |
364
|
|
|
} |
365
|
|
|
return $slice; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Inserts an object at a certain point |
371
|
|
|
* |
372
|
|
|
* @see http://stackoverflow.com/a/8736013 |
373
|
|
|
* @param mixed $object A single object |
374
|
|
|
* @param int $index |
375
|
|
|
* @param mixed $identifier |
376
|
|
|
* @return bool |
377
|
|
|
* @throws DuplicateCollectionIdentifierException |
378
|
|
|
* @throws InvalidEntityException |
379
|
|
|
*/ |
380
|
|
|
public function insertObjectAt($object, $index, $identifier = null) |
381
|
|
|
{ |
382
|
|
|
// check to ensure that objects don't already exist in the collection |
383
|
|
|
if ($this->has($identifier)) { |
384
|
|
|
throw new DuplicateCollectionIdentifierException($identifier); |
385
|
|
|
} |
386
|
|
|
// detach any objects at or past this index |
387
|
|
|
$remaining_objects = array(); |
388
|
|
|
if ($index < $this->count()) { |
389
|
|
|
$remaining_objects = $this->slice($index, $this->count() - $index); |
390
|
|
|
foreach ($remaining_objects as $key => $remaining_object) { |
391
|
|
|
// we need to grab the identifiers for each object and use them as keys |
392
|
|
|
$remaining_objects[ $remaining_object->getInfo() ] = $remaining_object; |
393
|
|
|
// and then remove the object from the current tracking array |
394
|
|
|
unset($remaining_objects[ $key ]); |
395
|
|
|
// and then remove it from the Collection |
396
|
|
|
$this->detach($remaining_object); |
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
// add the new object we're splicing in |
400
|
|
|
$this->add($object, $identifier); |
401
|
|
|
// attach the objects we previously detached |
402
|
|
|
foreach ($remaining_objects as $key => $remaining_object) { |
403
|
|
|
$this->add($remaining_object, $key); |
404
|
|
|
} |
405
|
|
|
return $this->contains($object); |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Inserts an object (or an array of objects) at a certain point |
411
|
|
|
* |
412
|
|
|
* @see http://stackoverflow.com/a/8736013 |
413
|
|
|
* @param mixed $objects A single object or an array of objects |
414
|
|
|
* @param int $index |
415
|
|
|
*/ |
416
|
|
|
public function insertAt($objects, $index) |
417
|
|
|
{ |
418
|
|
|
if (! is_array($objects)) { |
419
|
|
|
$objects = array($objects); |
420
|
|
|
} |
421
|
|
|
// check to ensure that objects don't already exist in the collection |
422
|
|
|
foreach ($objects as $key => $object) { |
423
|
|
|
if ($this->contains($object)) { |
424
|
|
|
unset($objects[ $key ]); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
// do we have any objects left? |
428
|
|
|
if (! $objects) { |
|
|
|
|
429
|
|
|
return; |
430
|
|
|
} |
431
|
|
|
// detach any objects at or past this index |
432
|
|
|
$remaining = array(); |
433
|
|
|
if ($index < $this->count()) { |
434
|
|
|
$remaining = $this->slice($index, $this->count() - $index); |
435
|
|
|
foreach ($remaining as $object) { |
436
|
|
|
$this->detach($object); |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
// add the new objects we're splicing in |
440
|
|
|
foreach ($objects as $object) { |
441
|
|
|
$this->attach($object); |
442
|
|
|
} |
443
|
|
|
// attach the objects we previously detached |
444
|
|
|
foreach ($remaining as $object) { |
445
|
|
|
$this->attach($object); |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Removes the object at the given index |
452
|
|
|
* |
453
|
|
|
* @see http://stackoverflow.com/a/8736013 |
454
|
|
|
* @param int $index |
455
|
|
|
*/ |
456
|
|
|
public function removeAt($index) |
457
|
|
|
{ |
458
|
|
|
$this->detach($this->objectAtIndex($index)); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* detaches ALL objects from the Collection |
464
|
|
|
*/ |
465
|
|
|
public function detachAll() |
466
|
|
|
{ |
467
|
|
|
$this->rewind(); |
468
|
|
|
while ($this->valid()) { |
469
|
|
|
$object = $this->current(); |
470
|
|
|
$this->next(); |
471
|
|
|
$this->detach($object); |
472
|
|
|
} |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
|
476
|
|
|
/** |
477
|
|
|
* unsets and detaches ALL objects from the Collection |
478
|
|
|
*/ |
479
|
|
|
public function trashAndDetachAll() |
480
|
|
|
{ |
481
|
|
|
$this->rewind(); |
482
|
|
|
while ($this->valid()) { |
483
|
|
|
$object = $this->current(); |
484
|
|
|
$this->next(); |
485
|
|
|
$this->detach($object); |
486
|
|
|
unset($object); |
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
} |
490
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.