Total Complexity | 54 |
Total Lines | 467 |
Duplicated Lines | 9.85 % |
Changes | 3 | ||
Bugs | 0 | Features | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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 |
||
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) |
|
|
|||
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) |
|
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() |
||
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() |
||
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) |
|
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) |
|
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)); |
||
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() |
||
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) |
||
494 | } |
||
495 | } |
||
496 |
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.