Collection   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 744
Duplicated Lines 2.02 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 99.17%

Importance

Changes 0
Metric Value
wmc 90
lcom 1
cbo 2
dl 15
loc 744
ccs 239
cts 241
cp 0.9917
rs 1.856
c 0
b 0
f 0

47 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 5
A get_type() 0 3 1
A add() 0 15 3
A clear() 0 3 1
A contains() 0 3 1
A find() 0 5 2
A find_index() 0 12 3
A at() 0 5 1
A index_exists() 0 11 3
A filter() 0 11 3
A find_last() 0 5 2
A find_last_index() 0 12 3
B slice() 0 21 7
A insert() 0 11 1
A insert_range() 0 16 2
A reject() 0 7 1
A remove_at() 0 12 1
A reverse() 0 5 1
A sort() 0 5 1
A to_array() 0 3 1
A reduce() 0 3 1
A every() 0 14 3
A drop() 0 7 2
A drop_right() 0 5 2
A drop_while() 0 4 2
A tail() 0 3 1
A take() 0 3 1
A take_right() 0 3 1
A take_while() 0 5 2
A each() 0 5 2
A map() 0 19 5
A shuffle() 0 6 1
A merge() 15 15 3
A first() 0 7 2
A last() 0 7 2
A count() 0 3 1
A serialize() 0 9 2
A current() 0 3 1
A next() 0 3 1
A key() 0 3 1
A valid() 0 3 1
A rewind() 0 3 1
A new_from_trusted() 0 6 2
A set_from_trusted() 0 3 1
A count_while_true() 0 12 3
A validate_index() 0 7 2
A reduce_right() 0 7 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
namespace Intraxia\Jaxion\Axolotl;
3
4
use Intraxia\Jaxion\Contract\Axolotl\Collection as CollectionContract;
5
use Intraxia\Jaxion\Contract\Axolotl\Serializes;
6
use InvalidArgumentException;
7
use OutOfBoundsException;
8
use OutOfRangeException;
9
10
/**
11
 * Class Collection
12
 *
13
 * @package Intraxia\Jaxion
14
 * @subpackage Axolotl
15
 */
16
class Collection implements CollectionContract {
17
18
	/**
19
	 * Collection elements.
20
	 *
21
	 * @var array
22
	 */
23
	protected $elements = array();
24
25
	/**
26
	 * Collection type to enforce.
27
	 *
28
	 * @var Type
29
	 */
30
	private $type;
31
32
	/**
33
	 * Where Collection is in loop.
34
	 *
35
	 * @var int
36
	 */
37
	protected $position = 0;
38
39
	/**
40
	 * Collection constructor.
41
	 *
42
	 * @param string $type
43
	 * @param array  $elements
44
	 */
45 312
	public function __construct( $type, array $elements = array() ) {
46 312
		$this->type = new Type( $type );
47
48 309
		if ( $this->type->is_model() ) {
49 33
			foreach ( $elements as $idx => $element ) {
50 27
				if ( is_array( $element ) ) {
51 9
					$elements[ $idx ] = $this->type->create_model( $element );
52 6
				}
53 22
			}
54 22
		}
55
56 309
		if ( $elements ) {
57 216
			$this->type->validate_elements( $elements );
58 144
		}
59
60 309
		$this->elements = $elements;
61 309
	}
62
63
	/**
64
	 * {@inheritdoc}
65
	 *
66
	 * @return string
67
	 */
68 144
	public function get_type() {
69 144
		return $this->type->get_type();
70
	}
71
72
	/**
73
	 * {@inheritdoc}
74
	 *
75
	 * @param mixed $element
76
	 *
77
	 * @return Collection
78
	 *
79
	 * @throws InvalidArgumentException
80
	 */
81 51
	public function add( $element ) {
82 51
		if ( $this->type->is_model() && is_array( $element ) ) {
83 3
			$element = $this->type->create_model( $element );
84 2
		}
85
86 51
		$this->type->validate_element( $element );
87
88 45
		$elements   = $this->elements;
89 45
		$elements[] = $element;
90
91 45
		$collection = new static( $this->get_type() );
92 45
		$collection->set_from_trusted( $elements );
93
94 45
		return $collection;
95
	}
96
97
	/**
98
	 * {@inheritdoc}
99
	 *
100
	 * @return Collection
101
	 */
102 6
	public function clear() {
103 6
		return new static( $this->get_type() );
104
	}
105
106
	/**
107
	 * {@inheritdoc}
108
	 *
109
	 * @param  callable $condition Condition to satisfy.
110
	 *
111
	 * @return bool
112
	 */
113 9
	public function contains( $condition ) {
114 9
		return (bool) $this->find( $condition );
115
	}
116
117
	/**
118
	 * {@inheritdoc}
119
	 *
120
	 * @param  callable $condition Condition to satisfy.
121
	 *
122
	 * @return mixed
123
	 */
124 15
	public function find( $condition ) {
125 15
		$index = $this->find_index( $condition );
126
127 15
		return -1 === $index ? null : $this->elements[ $index ];
128
	}
129
130
	/**
131
	 * {@inheritdoc}
132
	 *
133
	 * @param  callable $condition Condition to satisfy.
134
	 *
135
	 * @return int
136
	 */
137 21
	public function find_index( $condition ) {
138 21
		$index = -1;
139
140 21
		for ( $i = 0, $count = count( $this->elements ); $i < $count; $i++ ) {
141 21
			if ( call_user_func( $condition, ($this->at( $i ) ) ) ) {
142 12
				$index = $i;
143 12
				break;
144
			}
145 10
		}
146
147 21
		return $index;
148
	}
149
150
	/**
151
	 * Fetches the element at the provided index.
152
	 *
153
	 * @param int $index Index to get element from.
154
	 *
155
	 * @return mixed
156
	 *
157
	 * @throws OutOfRangeException
158
	 */
159 99
	public function at( $index ) {
160 99
		$this->validate_index( $index );
161
162 90
		return $this->elements[ $index ];
163
	}
164
165
	/**
166
	 * {@inheritdoc}
167
	 *
168
	 * @param  int $index Index to check for existence.
169
	 *
170
	 * @return bool
171
	 *
172
	 * @throws InvalidArgumentException
173
	 */
174 111
	public function index_exists( $index ) {
175 111
		if ( ! is_int( $index ) ) {
176 6
			throw new InvalidArgumentException( 'Index must be an integer' );
177
		}
178
179 105
		if ( $index < 0 ) {
180 6
			throw new InvalidArgumentException( 'Index must be a non-negative integer' );
181
		}
182
183 99
		return $index < $this->count();
184
	}
185
186
	/**
187
	 * {@inheritdoc}
188
	 *
189
	 * @param  callable $condition Condition to satisfy.
190
	 *
191
	 * @return Collection
192
	 */
193 6
	public function filter( $condition ) {
194 6
		$elements = array();
195
196 6
		foreach ( $this->elements as $element ) {
197 6
			if ( call_user_func( $condition, $element ) ) {
198 6
				$elements[] = $element;
199 4
			}
200 4
		}
201
202 6
		return $this->new_from_trusted( $elements );
203
	}
204
	/**
205
	 * {@inheritdoc}
206
	 *
207
	 * @param  callable $condition Condition to satisfy.
208
	 *
209
	 * @return mixed
210
	 */
211 6
	public function find_last( $condition ) {
212 6
		$index = $this->find_last_index( $condition );
213
214 6
		return -1 === $index ? null : $this->elements[ $index ];
215
	}
216
217
	/**
218
	 * {@inheritdoc}
219
	 *
220
	 * @param  callable $condition
221
	 * @return int
222
	 */
223 12
	public function find_last_index( $condition ) {
224 12
		$index = -1;
225
226 12
		for ( $i = count( $this->elements ) - 1; $i >= 0; $i-- ) {
227 12
			if ( call_user_func( $condition, $this->elements[ $i ] ) ) {
228 6
				$index = $i;
229 6
				break;
230
			}
231 6
		}
232
233 12
		return $index;
234
	}
235
236
	/**
237
	 * {@inheritdoc}
238
	 *
239
	 * @param  int $start Begining index to slice from.
240
	 * @param  int $end   End index to slice to.
241
	 *
242
	 * @return Collection
243
	 *
244
	 * @throws InvalidArgumentException
245
	 */
246 81
	public function slice( $start, $end ) {
247 81
		if ( $start < 0 || ! is_int( $start ) ) {
248 3
			throw new InvalidArgumentException( 'Start must be a non-negative integer' );
249
		}
250
251 78
		if ( $end < 0 || ! is_int( $end ) ) {
252 6
			throw new InvalidArgumentException( 'End must be a positive integer' );
253
		}
254
255 72
		if ( $start > $end ) {
256 3
			throw new InvalidArgumentException( 'End must be greater than start' );
257
		}
258
259 69
		if ( $end > $this->count() + 1 ) {
260 9
			throw new InvalidArgumentException( 'End must be less than the count of the items in the Collection' );
261
		}
262
263 60
		$length = $end - $start + 1;
264
265 60
		return $this->new_from_trusted( array_slice( $this->elements, $start, $length ) );
266
	}
267
268
	/**
269
	 * {@inheritdoc}
270
	 *
271
	 * @param int   $index     Index to start at.
272
	 * @param mixed $element Element to insert.
273
	 *
274
	 * @return Collection
275
	 *
276
	 * @throws InvalidArgumentException
277
	 * @throws OutOfRangeException
278
	 */
279 3
	public function insert( $index, $element ) {
280 3
		$this->validate_index( $index );
281 3
		$this->type->validate_element( $element );
282
283 3
		$a = array_slice( $this->elements, 0, $index );
284 3
		$b = array_slice( $this->elements, $index, count( $this->elements ) );
285
286 3
		$a[] = $element;
287
288 3
		return $this->new_from_trusted( array_merge( $a, $b ) );
289
	}
290
291
	/**
292
	 * {@inheritdoc}
293
	 *
294
	 * @param int   $index    Index to start insertion at.
295
	 * @param array $elements Elements in insert.
296
	 *
297
	 * @return Collection
298
	 *
299
	 * @throws OutOfRangeException
300
	 */
301 3
	public function insert_range( $index, array $elements ) {
302 3
		$this->validate_index( $index );
303 3
		$this->type->validate_elements( $elements );
304
305 3
		if ( $index < 0 ) {
306
			$index = $this->count() + $index + 1;
307
		}
308
309 3
		return $this->new_from_trusted(
310 3
			array_merge(
311 3
				array_slice( $this->elements, 0, $index ),
312 2
				$elements,
313 3
				array_slice( $this->elements, $index, count( $this->elements ) )
314 2
			)
315 2
		);
316
	}
317
318
	/**
319
	 * {@inheritdoc}
320
	 *
321
	 * @param  callable $condition Condition to satisfy.
322
	 *
323
	 * @return Collection
324
	 */
325 2
	public function reject( $condition ) {
326 1
		$inverse = function ( $element ) use ( $condition ) {
327 3
			return ! call_user_func( $condition, $element );
328 3
		};
329
330 3
		return $this->filter( $inverse );
331
	}
332
333
	/**
334
	 * {@inheritdoc}
335
	 *
336
	 * @param  int $index Index to remove.
337
	 *
338
	 * @return Collection
339
	 *
340
	 * @throws OutOfRangeException
341
	 */
342 3
	public function remove_at( $index ) {
343 3
		$this->validate_index( $index );
344
345 3
		$elements = $this->elements;
346
347 3
		return $this->new_from_trusted(
348 3
			array_merge(
349 3
				array_slice( $elements, 0, $index ),
350 3
				array_slice( $elements, $index + 1, count( $elements ) )
351 2
			)
352 2
		);
353
	}
354
	/**
355
	 * {@inheritdoc}
356
	 *
357
	 * @return Collection
358
	 */
359 3
	public function reverse() {
360 3
		return $this->new_from_trusted(
361 3
			array_reverse( $this->elements )
362 2
		);
363
	}
364
365
	/**
366
	 * {@inheritdoc}
367
	 *
368
	 * @param callable $callback Sort callback.
369
	 *
370
	 * @return Collection
371
	 */
372 3
	public function sort( $callback ) {
373 3
		$elements = $this->elements;
374 3
		usort( $elements, $callback );
375 3
		return $this->new_from_trusted( $elements );
376
	}
377
378
	/**
379
	 * {@inheritdoc}
380
	 *
381
	 * @return array
382
	 */
383 30
	public function to_array() {
384 30
		return $this->elements;
385
	}
386
387
	/**
388
	 * {@inheritdoc}
389
	 *
390
	 * @param callable $callable Reducer function.
391
	 *
392
	 * @param null     $initial  Initial reducer value.
393
	 *
394
	 * @return mixed
395
	 */
396 3
	public function reduce( $callable, $initial = null ) {
397 3
		return array_reduce( $this->elements, $callable, $initial );
398
	}
399
400
	/**
401
	 * {@inheritdoc}
402
	 *
403
	 * @param callable $condition Condition callback.
404
	 *
405
	 * @return bool
406
	 */
407 6
	public function every( $condition ) {
408 6
		$response = true;
409
410 6
		foreach ( $this->elements as $element ) {
411 6
			$result = call_user_func( $condition, $element );
412
413 6
			if ( false === $result ) {
414 3
				$response = false;
415 3
				break;
416
			}
417 4
		}
418
419 6
		return $response;
420
	}
421
422
	/**
423
	 * {@inheritdoc}
424
	 *
425
	 * @param  int $num Number of elements to drop.
426
	 *
427
	 * @return Collection
428
	 *
429
	 * @throws InvalidArgumentException
430
	 */
431 18
	public function drop( $num ) {
432 18
		if ( $num > $this->count() ) {
433 3
			$num = $this->count();
434 2
		}
435
436 18
		return $this->slice( $num, $this->count() );
437
	}
438
439
	/**
440
	 * {@inheritdoc}
441
	 *
442
	 * @param int $num Number of elements to drop.
443
	 *
444
	 * @return Collection
445
	 *
446
	 * @throws InvalidArgumentException
447
	 */
448 9
	public function drop_right( $num ) {
449 9
		return $num !== $this->count()
450 9
			? $this->slice( 0, $this->count() - $num - 1 )
451 9
			: $this->clear();
452
	}
453
454
	/**
455
	 * {@inheritdoc}
456
	 *
457
	 * @param callable $condition Condition callback.
458
	 *
459
	 * @return Collection
460
	 */
461 9
	public function drop_while( $condition ) {
462 9
		$count = $this->count_while_true( $condition );
463 9
		return $count ? $this->drop( $count ) : $this;
464
	}
465
	/**
466
	 * {@inheritdoc}
467
	 *
468
	 * @return Collection
469
	 *
470
	 * @throws InvalidArgumentException
471
	 */
472 3
	public function tail() {
473 3
		return $this->slice( 1, $this->count() );
474
	}
475
476
	/**
477
	 * {@inheritdoc}
478
	 *
479
	 * @param  int $num Number of elements to take.
480
	 *
481
	 * @return Collection
482
	 *
483
	 * @throws InvalidArgumentException
484
	 */
485 18
	public function take( $num ) {
486 18
		return $this->slice( 0, $num - 1 );
487
	}
488
489
	/**
490
	 * {@inheritdoc}
491
	 *
492
	 * @param int $num Number of elements to take.
493
	 *
494
	 * @return Collection
495
	 *
496
	 * @throws InvalidArgumentException
497
	 */
498 3
	public function take_right( $num ) {
499 3
		return $this->slice( $this->count() - $num, $this->count() );
500
	}
501
502
	/**
503
	 * {@inheritdoc}
504
	 *
505
	 * @param callable $condition Callback function.
506
	 *
507
	 * @return Collection
508
	 */
509 3
	public function take_while( $condition ) {
510 3
		$count = $this->count_while_true( $condition );
511
512 3
		return $count ? $this->take( $count ) : $this->clear();
513
	}
514
515
	/**
516
	 * {@inheritdoc}
517
	 *
518
	 * @param callable $callable Callback function.
519
	 */
520 3
	public function each( $callable ) {
521 3
		foreach ( $this->elements as $element ) {
522 3
			call_user_func( $callable, $element );
523 2
		}
524 3
	}
525
526
	/**
527
	 * {@inheritdoc}
528
	 *
529
	 * @param callable $callable Callback function.
530
	 *
531
	 * @return Collection
532
	 */
533 18
	public function map( $callable ) {
534 18
		$elements = array();
535 18
		$type = null;
536 18
		foreach ( $this->elements as $element ) {
537 15
			$result = call_user_func( $callable, $element );
538
539 15
			if ( null === $type ) {
540 15
				$type = gettype( $result );
541
542 15
				if ( 'object' === $type ) {
543 3
					$type = get_class( $result );
544 2
				}
545 10
			}
546
547 15
			$elements[] = $result;
548 12
		}
549
550 18
		return $this->new_from_trusted( $elements, $type ? : $this->get_type() );
551
	}
552
553
	/**
554
	 * {@inheritdoc}
555
	 *
556
	 * @param callable $callable Reducer function.
557
	 * @param null     $initial  Initial value.
558
	 *
559
	 * @return mixed
560
	 */
561 15
	public function reduce_right( $callable, $initial = null ) {
562 15
		return array_reduce(
563 15
			array_reverse( $this->elements ),
564 10
			$callable,
565
			$initial
566 10
		);
567
	}
568
569
	/**
570
	 * {@inheritdoc}
571
	 *
572
	 * @return Collection
573
	 */
574 3
	public function shuffle() {
575 3
		$elements = $this->elements;
576 3
		shuffle( $elements );
577
578 3
		return $this->new_from_trusted( $elements );
579
	}
580
581
	/**
582
	 * {@inheritdoc}
583
	 *
584
	 * @param array|Collection $elements Array of elements to merge.
585
	 *
586
	 * @return Collection
587
	 *
588
	 * @throws InvalidArgumentException
589
	 */
590 12 View Code Duplication
	public function merge( $elements ) {
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...
591 12
		if ( $elements instanceof static ) {
592 3
			$elements = $elements->to_array();
593 2
		}
594
595 12
		if ( ! is_array( $elements ) ) {
596 3
			throw new InvalidArgumentException( 'Merge must be given array or Collection' );
597
		}
598
599 9
		$this->type->validate_elements( $elements );
600
601 6
		return $this->new_from_trusted(
602 6
			array_merge( $this->elements, $elements )
603 4
		);
604
	}
605
606
	/**
607
	 * {@inheritdoc}
608
	 *
609
	 * @return mixed
610
	 *
611
	 * @throws OutOfBoundsException
612
	 */
613 6
	public function first() {
614 6
		if ( empty( $this->elements ) ) {
615 3
			throw new OutOfBoundsException( 'Cannot get first element of empty Collection' );
616
		}
617
618 3
		return reset( $this->elements );
619
	}
620
621
	/**
622
	 * {@inheritdoc}
623
	 *
624
	 * @return mixed
625
	 *
626
	 * @throws OutOfBoundsException
627
	 */
628 6
	public function last() {
629 6
		if ( empty( $this->elements ) ) {
630 3
			throw new OutOfBoundsException( 'Cannot get last element of empty Collection' );
631
		}
632
633 3
		return end( $this->elements );
634
	}
635
636
	/**
637
	 * {@inheritdoc}
638
	 *
639
	 * @return int
640
	 */
641 168
	public function count() {
642 168
		return count( $this->elements );
643
	}
644
645
	/**
646
	 * {@inheritDoc}
647
	 *
648
	 * @return array
649
	 */
650
	public function serialize() {
651 9
		return $this->map(function( $element ) {
652 6
			if ( $element instanceof Serializes ) {
653 3
				return $element->serialize();
654
			}
655
656 3
			return $element;
657 9
		} )->to_array();
658
	}
659
660
	/**
661
	 * Return the current element.
662
	 *
663
	 * @return mixed
664
	 */
665 3
	public function current() {
666 3
		return $this->at( $this->position );
667
	}
668
669
	/**
670
	 * Move forward to next element.
671
	 */
672 3
	public function next() {
673 3
		$this->position ++;
674 3
	}
675
676
	/**
677
	 * Return the key of the current element.
678
	 *
679
	 * @return mixed
680
	 */
681 3
	public function key() {
682 3
		return $this->position;
683
	}
684
685
	/**
686
	 * Checks if current position is valid.
687
	 *
688
	 * @return bool
689
	 */
690 3
	public function valid() {
691 3
		return isset( $this->elements[ $this->position ] );
692
	}
693
694
	/**
695
	 * Rewind the Iterator to the first element.
696
	 */
697 3
	public function rewind() {
698 3
		$this->position = 0;
699 3
	}
700
701
	/**
702
	 * Creates a new instance of the Collection
703
	 * from a trusted set of elements.
704
	 *
705
	 * @param array      $elements Array of elements to pass into new collection.
706
	 * @param null|mixed $type
707
	 *
708
	 * @return static
709
	 */
710 108
	protected function new_from_trusted( array $elements, $type = null ) {
711 108
		$collection = new static( null !== $type ? $type : $this->get_type() );
712 108
		$collection->set_from_trusted( $elements );
713
714 108
		return $collection;
715
	}
716
717
	/**
718
	 * Sets the elements without validating them.
719
	 *
720
	 * @param array $elements Pre-validated elements to set.
721
	 */
722 153
	protected function set_from_trusted( array $elements ) {
723 153
		$this->elements = $elements;
724 153
	}
725
726
	/**
727
	 * Number of elements true for the condition.
728
	 *
729
	 * @param callable $condition Condition to check.
730
	 * @return int
731
	 */
732 12
	protected function count_while_true( $condition ) {
733 12
		$count = 0;
734
735 12
		foreach ( $this->elements as $element ) {
736 12
			if ( ! $condition($element) ) {
737 9
				break;
738
			}
739 9
			$count++;
740 8
		}
741
742 12
		return $count;
743
	}
744
745
	/**
746
	 * Validates a number to be used as an index.
747
	 *
748
	 * @param  integer $index The number to be validated as an index.
749
	 *
750
	 * @throws OutOfRangeException
751
	 */
752 102
	protected function validate_index( $index ) {
753 102
		$exists = $this->index_exists( $index );
754
755 96
		if ( ! $exists ) {
756 6
			throw new OutOfRangeException( 'Index out of bounds of collection' );
757
		}
758 93
	}
759
}
760