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