Collection   C
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 467
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 96
dl 0
loc 467
rs 6.4799
c 3
b 0
f 0
wmc 54

28 Methods

Rating   Name   Duplication   Size   Complexity  
A keys() 0 3 1
A toBase() 0 3 1
A getCachingIterator() 0 3 1
A map() 0 3 1
A isEmpty() 0 3 1
A count() 0 3 1
A __construct() 0 4 2
A isAcceptable() 0 3 1
A values() 0 3 1
A getIterator() 0 3 1
A all() 0 3 1
A objects() 0 3 1
A clear() 0 5 1
A last() 0 7 2
A get() 0 11 3
A asArray() 0 13 5
A has() 0 7 2
A first() 0 7 2
A merge() 0 19 4
A remove() 0 9 2
A resolveOffset() 0 9 3
A modelKey() 0 12 3
A offsetExists() 0 10 2
A add() 0 15 3
A offsetGet() 0 11 3
A offsetUnset() 0 11 3
A offsetSet() 0 7 2
A pos() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Collection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Collection, and based on these observations, apply Extract Interface, too.

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
    public function get($key)
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
    public function remove($key)
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
    public function offsetGet($offset)
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
    public function offsetUnset($offset)
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)) {
0 ignored issues
show
introduced by
The condition is_int($offset) is always true.
Loading history...
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