Passed
Branch master (018ba4)
by Mathieu
06:43
created

Collection::asArray()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
nc 5
nop 1
dl 0
loc 13
rs 8.8571
c 1
b 0
f 0
1
<?php
2
3
namespace Charcoal\Model;
4
5
use Traversable;
6
use ArrayIterator;
7
use CachingIterator;
8
use LogicException;
9
use InvalidArgumentException;
10
11
// From 'charcoal-core'
12
use Charcoal\Model\CollectionInterface;
13
use Charcoal\Model\ModelInterface;
14
15
/**
16
 * A Model Collection
17
 *
18
 * For iterating instances of {@see ModelInterface}.
19
 *
20
 * Used by {@see \Charcoal\Loader\CollectionLoader} for storing results.
21
 *
22
 * The collection stores models by {@see \Charcoal\Source\StorableInterface their primary key}.
23
 * If two objects share the same storable ID but hold disparate data, they are considered
24
 * to be alike. Adding an object that shares the same ID as an object previously stored in
25
 * the collection will replace the latter.
26
 */
27
class Collection implements CollectionInterface
28
{
29
    /**
30
     * The objects contained in the collection.
31
     *
32
     * Stored as a dictionary indexed by each object's primary key.
33
     * Ensures that each object gets loaded only once by keeping
34
     * every loaded object in an associative array.
35
     *
36
     * @var object[]
37
     */
38
    protected $objects = [];
39
40
    /**
41
     * Create a new collection.
42
     *
43
     * @param  array|Traversable|null $objs Array of objects to pre-populate this collection.
44
     * @return void
45
     */
46
    public function __construct($objs = null)
47
    {
48
        if ($objs) {
49
            $this->merge($objs);
50
        }
51
    }
52
53
    /**
54
     * Retrieve the first object in the collection.
55
     *
56
     * @return object|null Returns the first object, or NULL if the collection is empty.
57
     */
58
    public function first()
59
    {
60
        if (empty($this->objects)) {
61
            return null;
62
        }
63
64
        return reset($this->objects);
65
    }
66
67
    /**
68
     * Retrieve the last object in the collection.
69
     *
70
     * @return object|null Returns the last object, or NULL if the collection is empty.
71
     */
72
    public function last()
73
    {
74
        if (empty($this->objects)) {
75
            return null;
76
        }
77
78
        return end($this->objects);
79
    }
80
81
    // Satisfies CollectionInterface
82
    // =============================================================================================
83
84
    /**
85
     * Merge the collection with the given objects.
86
     *
87
     * @param  array|Traversable $objs Array of objects to append to this collection.
88
     * @throws InvalidArgumentException If the given array contains an unacceptable value.
89
     * @return self
90
     */
91
    public function merge($objs)
92
    {
93
        $objs = $this->asArray($objs);
94
95
        foreach ($objs as $obj) {
96
            if (!$this->isAcceptable($obj)) {
97
                throw new InvalidArgumentException(
98
                    sprintf(
99
                        'Must be an array of models, contains %s',
100
                        (is_object($obj) ? get_class($obj) : gettype($obj))
101
                    )
102
                );
103
            }
104
105
            $key = $this->modelKey($obj);
106
            $this->objects[$key] = $obj;
107
        }
108
109
        return $this;
110
    }
111
112
    /**
113
     * Add an object to the collection.
114
     *
115
     * @param  object $obj An acceptable object.
116
     * @throws InvalidArgumentException If the given object is not acceptable.
117
     * @return self
118
     */
119
    public function add($obj)
120
    {
121
        if (!$this->isAcceptable($obj)) {
122
            throw new InvalidArgumentException(
123
                sprintf(
124
                    'Must be a model, received %s',
125
                    (is_object($obj) ? get_class($obj) : gettype($obj))
126
                )
127
            );
128
        }
129
130
        $key = $this->modelKey($obj);
131
        $this->objects[$key] = $obj;
132
133
        return $this;
134
    }
135
136
    /**
137
     * Retrieve the object by primary key.
138
     *
139
     * @param  mixed $key The primary key.
140
     * @return object|null Returns the requested object or NULL if not in the collection.
141
     */
142 View Code Duplication
    public function get($key)
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...
143
    {
144
        if ($this->isAcceptable($key)) {
145
            $key = $this->modelKey($key);
146
        }
147
148
        if ($this->has($key)) {
149
            return $this->objects[$key];
150
        }
151
152
        return null;
153
    }
154
155
    /**
156
     * Determine if an object exists in the collection by key.
157
     *
158
     * @param  mixed $key The primary key to lookup.
159
     * @return boolean
160
     */
161
    public function has($key)
162
    {
163
        if ($this->isAcceptable($key)) {
164
            $key = $this->modelKey($key);
165
        }
166
167
        return array_key_exists($key, $this->objects);
168
    }
169
170
    /**
171
     * Remove object from collection by primary key.
172
     *
173
     * @param  mixed $key The object primary key to remove.
174
     * @throws InvalidArgumentException If the given key is not acceptable.
175
     * @return self
176
     */
177 View Code Duplication
    public function remove($key)
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...
178
    {
179
        if ($this->isAcceptable($key)) {
180
            $key = $this->modelKey($key);
181
        }
182
183
        unset($this->objects[$key]);
184
185
        return $this;
186
    }
187
188
    /**
189
     * Remove all objects from collection.
190
     *
191
     * @return self
192
     */
193
    public function clear()
194
    {
195
        $this->objects = [];
196
197
        return $this;
198
    }
199
200
    /**
201
     * Retrieve all objects in collection indexed by primary keys.
202
     *
203
     * @return object[] An associative array of objects.
204
     */
205
    public function all()
206
    {
207
        return $this->objects;
208
    }
209
210
    /**
211
     * Retrieve all objects in the collection indexed numerically.
212
     *
213
     * @return object[] A sequential array of objects.
214
     */
215
    public function values()
216
    {
217
        return array_values($this->objects);
218
    }
219
220
    /**
221
     * Retrieve the primary keys of the objects in the collection.
222
     *
223
     * @return array A sequential array of keys.
224
     */
225
    public function keys()
226
    {
227
        return array_keys($this->objects);
228
    }
229
230
    // Satisfies ArrayAccess
231
    // =============================================================================================
232
233
    /**
234
     * Alias of {@see CollectionInterface::has()}.
235
     *
236
     * @see    \ArrayAccess
237
     * @param  mixed $offset The object primary key or array offset.
238
     * @return boolean
239
     */
240
    public function offsetExists($offset)
241
    {
242
        if (is_int($offset)) {
243
            $offset  = $this->resolveOffset($offset);
244
            $objects = array_values($this->objects);
245
246
            return array_key_exists($offset, $objects);
247
        }
248
249
        return $this->has($offset);
250
    }
251
252
    /**
253
     * Alias of {@see CollectionInterface::get()}.
254
     *
255
     * @see    \ArrayAccess
256
     * @param  mixed $offset The object primary key or array offset.
257
     * @return mixed Returns the requested object or NULL if not in the collection.
258
     */
259 View Code Duplication
    public function offsetGet($offset)
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...
260
    {
261
        if (is_int($offset)) {
262
            $offset  = $this->resolveOffset($offset);
263
            $objects = array_values($this->objects);
264
            if (isset($objects[$offset])) {
265
                return $objects[$offset];
266
            }
267
        }
268
269
        return $this->get($offset);
270
    }
271
272
    /**
273
     * Alias of {@see CollectionInterface::set()}.
274
     *
275
     * @see    \ArrayAccess
276
     * @param  mixed $offset The object primary key or array offset.
277
     * @param  mixed $value  The object.
278
     * @throws LogicException Attempts to assign an offset.
279
     * @return void
280
     */
281
    public function offsetSet($offset, $value)
282
    {
283
        if ($offset === null) {
284
            $this->add($value);
285
        } else {
286
            throw new LogicException(
287
                sprintf('Offsets are not accepted on the model collection, received %s.', $offset)
288
            );
289
        }
290
    }
291
292
    /**
293
     * Alias of {@see CollectionInterface::remove()}.
294
     *
295
     * @see    \ArrayAccess
296
     * @param  mixed $offset The object primary key or array offset.
297
     * @return void
298
     */
299 View Code Duplication
    public function offsetUnset($offset)
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...
300
    {
301
        if (is_int($offset)) {
302
            $offset = $this->resolveOffset($offset);
303
            $keys   = array_keys($this->objects);
304
            if (isset($keys[$offset])) {
305
                $offset = $keys[$offset];
306
            }
307
        }
308
309
        $this->remove($offset);
310
    }
311
312
    /**
313
     * Parse the array offset.
314
     *
315
     * If offset is non-negative, the sequence will start at that offset in the collection.
316
     * If offset is negative, the sequence will start that far from the end of the collection.
317
     *
318
     * @param  integer $offset The array offset.
319
     * @return integer Returns the resolved array offset.
320
     */
321
    protected function resolveOffset($offset)
322
    {
323
        if (is_int($offset)) {
324
            if ($offset < 0) {
325
                $offset = ($this->count() - abs($offset));
326
            }
327
        }
328
329
        return $offset;
330
    }
331
332
    // Satisfies Countable
333
    // =============================================================================================
334
335
    /**
336
     * Get number of objects in collection
337
     *
338
     * @see    \Countable
339
     * @return integer
340
     */
341
    public function count()
342
    {
343
        return count($this->objects);
344
    }
345
346
    // Satisfies IteratorAggregate
347
    // =============================================================================================
348
349
    /**
350
     * Retrieve an external iterator.
351
     *
352
     * @see    \IteratorAggregate
353
     * @return \ArrayIterator
354
     */
355
    public function getIterator()
356
    {
357
        return new ArrayIterator($this->objects);
358
    }
359
360
    /**
361
     * Retrieve a cached iterator.
362
     *
363
     * @param  integer $flags Bitmask of flags.
364
     * @return \CachingIterator
365
     */
366
    public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING)
367
    {
368
        return new CachingIterator($this->getIterator(), $flags);
369
    }
370
371
    // Satisfies backwards-compatibility
372
    // =============================================================================================
373
374
    /**
375
     * Retrieve the array offset from the given key.
376
     *
377
     * @deprecated
378
     * @param  mixed $key The primary key to retrieve the offset from.
379
     * @return integer Returns an array offset.
380
     */
381
    public function pos($key)
382
    {
383
        trigger_error('Collection::pos() is deprecated', E_USER_DEPRECATED);
384
385
        return array_search($key, array_keys($this->objects));
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_search($key...y_keys($this->objects)) also could return the type false|string which is incompatible with the documented return type integer.
Loading history...
386
    }
387
388
    /**
389
     * Alias of {@see self::values()}
390
     *
391
     * @deprecated
392
     * @todo   Trigger deprecation error.
393
     * @return object[]
394
     */
395
    public function objects()
396
    {
397
        return $this->values();
398
    }
399
400
    /**
401
     * Alias of {@see self::all()}.
402
     *
403
     * @deprecated
404
     * @todo   Trigger deprecation error.
405
     * @return object[]
406
     */
407
    public function map()
408
    {
409
        return $this->all();
410
    }
411
412
    // =============================================================================================
413
414
    /**
415
     * Determine if the given value is acceptable for the collection.
416
     *
417
     * Note: Practical for specialized collections extending the base collection.
418
     *
419
     * @param  mixed $value The value being vetted.
420
     * @return boolean
421
     */
422
    public function isAcceptable($value)
423
    {
424
        return ($value instanceof ModelInterface);
425
    }
426
427
    /**
428
     * Convert a given object into a model identifier.
429
     *
430
     * Note: Practical for specialized collections extending the base collection.
431
     *
432
     * @param  object $obj An acceptable object.
433
     * @throws InvalidArgumentException If the given object is not acceptable.
434
     * @return boolean
435
     */
436
    protected function modelKey($obj)
437
    {
438
        if (!$this->isAcceptable($obj)) {
439
            throw new InvalidArgumentException(
440
                sprintf(
441
                    'Must be a model, received %s',
442
                    (is_object($obj) ? get_class($obj) : gettype($obj))
443
                )
444
            );
445
        }
446
447
        return $obj->id();
448
    }
449
450
    /**
451
     * Determine if the collection is empty or not.
452
     *
453
     * @return boolean
454
     */
455
    public function isEmpty()
456
    {
457
        return empty($this->objects);
458
    }
459
460
    /**
461
     * Get a base collection instance from this collection.
462
     *
463
     * Note: Practical for extended classes.
464
     *
465
     * @return Collection
466
     */
467
    public function toBase()
468
    {
469
        return new self($this);
470
    }
471
472
    /**
473
     * Parse the given value into an array.
474
     *
475
     * @link http://php.net/types.array#language.types.array.casting
476
     *     If an object is converted to an array, the result is an array whose
477
     *     elements are the object's properties.
478
     * @param  mixed $value The value being converted.
479
     * @return array
480
     */
481
    protected function asArray($value)
482
    {
483
        if (is_array($value)) {
484
            return $value;
485
        } elseif ($value instanceof CollectionInterface) {
486
            return $value->all();
487
        } elseif ($value instanceof Traversable) {
488
            return iterator_to_array($value);
489
        } elseif ($value instanceof ModelInterface) {
490
            return [ $value ];
491
        }
492
493
        return (array)$value;
494
    }
495
}
496