Completed
Pull Request — master (#16)
by James
04:17 queued 02:31
created

Collection::next()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
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
	public function __construct( $type, array $elements = array() ) {
46
		$this->type = new Type( $type );
47
48
		if ( $this->type->is_model() ) {
49
			foreach ( $elements as $idx => $element ) {
50
				if ( is_array( $element ) ) {
51 60
					$elements[ $idx ] = $this->type->create_model( $element );
52 60
				}
53
			}
54 57
		}
55 24
56 57
		if ( $elements ) {
57 57
			$this->type->validate_elements( $elements );
58
		}
59
60
		$this->elements = $elements;
61
	}
62
63
	/**
64
	 * {@inheritdoc}
65
	 *
66
	 * @return string
67
	 */
68 27
	public function get_type() {
69 27
		return $this->type->get_type();
70 9
	}
71 9
72
	/**
73 27
	 * {@inheritdoc}
74 3
	 *
75
	 * @param mixed $element
76
	 *
77 24
	 * @return Collection
78
	 *
79 24
	 * @throws InvalidArgumentException
80
	 */
81
	public function add( $element ) {
82
		if ( $this->type->is_model() && is_array( $element ) ) {
83
			$element = $this->type->create_model( $element );
84
		}
85
86
		$this->type->validate_element( $element );
87
88
		$elements   = $this->elements;
89
		$elements[] = $element;
90
91
		$collection = new static( $this->get_type() );
92
		$collection->set_from_trusted( $elements );
93
94
		return $collection;
95
	}
96
97
	/**
98
	 * {@inheritdoc}
99
	 *
100
	 * @return Collection
101
	 */
102
	public function clear() {
103
		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
	public function contains( $condition ) {
114 12
		return (bool) $this->find( $condition );
115 12
	}
116
117
	/**
118
	 * {@inheritdoc}
119
	 *
120
	 * @param  callable $condition Condition to satisfy.
121
	 *
122
	 * @return mixed
123
	 */
124
	public function find( $condition ) {
125
		$index = $this->find_index( $condition );
126
127
		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
	public function find_index( $condition ) {
138
		$index = -1;
139
140
		for ( $i = 0, $count = count( $this->elements ); $i < $count; $i++ ) {
141
			if ( call_user_func( $condition, ($this->at( $i ) ) ) ) {
142
				$index = $i;
143
				break;
144
			}
145
		}
146
147
		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
	public function at( $index ) {
160
		$this->validate_index( $index );
161
162
		return $this->elements[ $index ];
163 9
	}
164 6
165 3
	/**
166
	 * {@inheritdoc}
167
	 *
168 3
	 * @param  int $index Index to check for existence.
169 9
	 *
170
	 * @return bool
171
	 *
172
	 * @throws InvalidArgumentException
173
	 */
174
	public function index_exists( $index ) {
175
		if ( ! is_int( $index ) ) {
176
			throw new InvalidArgumentException( 'Index must be an integer' );
177 3
		}
178 3
179
		if ( $index < 0 ) {
180
			throw new InvalidArgumentException( 'Index must be a non-negative integer' );
181
		}
182
183
		return $index < $this->count();
184 3
	}
185 3
186 3
	/**
187
	 * {@inheritdoc}
188
	 *
189
	 * @param  callable $condition Condition to satisfy.
190
	 *
191
	 * @return mixed
192
	 */
193 3
	public function filter( $condition ) {
194 3
		$elements = array();
195
196
		foreach ( $this->elements as $element ) {
197
			if ( call_user_func( $condition, $element ) ) {
198
				$elements[] = $element;
199
			}
200
		}
201
202 3
		return $this->new_from_trusted( $elements );
203 3
	}
204
	/**
205
	 * {@inheritdoc}
206
	 *
207
	 * @param  callable $condition Condition to satisfy.
208
	 *
209 3
	 * @return mixed
210 3
	 */
211 3
	public function find_last( $condition ) {
212
		$index = $this->find_last_index( $condition );
213
214
		return -1 === $index ? null : $this->elements[ $index ];
215
	}
216
217
	/**
218 6
	 * {@inheritdoc}
219 6
	 *
220
	 * @param  callable $condition
221
	 * @return int
222
	 */
223
	public function find_last_index( $condition ) {
224
		$index = -1;
225
226
		for ( $i = count( $this->elements ) - 1; $i >= 0; $i-- ) {
227
			if ( call_user_func( $condition, $this->elements[ $i ] ) ) {
228
				$index = $i;
229 60
				break;
230 60
			}
231 15
		}
232
233 15
		return $index;
234 3
	}
235
236
	/**
237 12
	 * {@inheritdoc}
238 12
	 *
239 57
	 * @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
	public function slice( $start, $end ) {
247
		if ( $start < 0 || ! is_int( $start ) ) {
248
			throw new InvalidArgumentException( 'Start must be a non-negative integer' );
249
		}
250
251
		if ( $end < 0 || ! is_int( $end ) ) {
252
			throw new InvalidArgumentException( 'End must be a positive integer' );
253
		}
254
255
		if ( $start > $end ) {
256
			throw new InvalidArgumentException( 'End must be greater than start' );
257
		}
258
259
		if ( $end > $this->count() + 1 ) {
260
			throw new InvalidArgumentException( 'End must be less than the count of the items in the Collection' );
261
		}
262
263
		$length = $end - $start + 1;
264
265
		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
	public function insert( $index, $element ) {
280
		$this->validate_index( $index );
281
		$this->type->validate_element( $element );
282
283
		$a = array_slice( $this->elements, 0, $index );
284
		$b = array_slice( $this->elements, $index, count( $this->elements ) );
285
286
		$a[] = $element;
287
288
		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
	public function insert_range( $index, array $elements ) {
302
		$this->validate_index( $index );
303
		$this->type->validate_elements( $elements );
304
305
		if ( $index < 0 ) {
306
			$index = $this->count() + $index + 1;
307
		}
308
309
		return $this->new_from_trusted(
310
			array_merge(
311
				array_slice( $this->elements, 0, $index ),
312
				$elements,
313
				array_slice( $this->elements, $index, count( $this->elements ) )
314
			)
315
		);
316
	}
317
318
	/**
319
	 * {@inheritdoc}
320
	 *
321
	 * @param  callable $condition Condition to satisfy.
322
	 *
323
	 * @return Collection
324
	 */
325
	public function reject( $condition ) {
326
		$inverse = function ( $element ) use ( $condition ) {
327
			return ! call_user_func( $condition, $element );
328
		};
329
330
		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
	public function remove_at( $index ) {
343
		$this->validate_index( $index );
344
345
		$elements = $this->elements;
346
347
		return $this->new_from_trusted(
348
			array_merge(
349
				array_slice( $elements, 0, $index ),
350
				array_slice( $elements, $index + 1, count( $elements ) )
351
			)
352
		);
353
	}
354
	/**
355
	 * {@inheritdoc}
356
	 *
357
	 * @return Collection
358
	 */
359
	public function reverse() {
360
		return $this->new_from_trusted(
361
			array_reverse( $this->elements )
362
		);
363
	}
364
365
	/**
366
	 * {@inheritdoc}
367
	 *
368
	 * @param callable $callback Sort callback.
369
	 *
370
	 * @return Collection
371
	 */
372
	public function sort( $callback ) {
373
		$elements = $this->elements;
374
		usort( $elements, $callback );
375
		return $this->new_from_trusted( $elements );
376
	}
377
378
	/**
379
	 * {@inheritdoc}
380
	 *
381
	 * @return array
382
	 */
383
	public function to_array() {
384
		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
	public function reduce( $callable, $initial = null ) {
397
		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
	public function every( $condition ) {
408
		$response = true;
409
410
		foreach ( $this->elements as $element ) {
411
			$result = call_user_func( $condition, $element );
412
413
			if ( false === $result ) {
414
				$response = false;
415
				break;
416
			}
417
		}
418
419
		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
	public function drop( $num ) {
432
		if ( $num > $this->count() ) {
433
			$num = $this->count();
434
		}
435
436
		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
	public function drop_right( $num ) {
449
		return $num !== $this->count()
450
			? $this->slice( 0, $this->count() - $num - 1 )
451
			: $this->clear();
452
	}
453
454
	/**
455
	 * {@inheritdoc}
456
	 *
457
	 * @param callable $condition Condition callback.
458
	 *
459
	 * @return Collection
460
	 */
461
	public function drop_while( $condition ) {
462
		$count = $this->count_while_true( $condition );
463
		return $count ? $this->drop( $count ) : $this;
464
	}
465
	/**
466
	 * {@inheritdoc}
467
	 *
468
	 * @return Collection
469
	 *
470
	 * @throws InvalidArgumentException
471
	 */
472
	public function tail() {
473
		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
	public function take( $num ) {
486
		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
	public function take_right( $num ) {
499
		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
	public function take_while( $condition ) {
510
		$count = $this->count_while_true( $condition );
511
512
		return $count ? $this->take( $count ) : $this->clear();
513
	}
514
515
	/**
516
	 * {@inheritdoc}
517
	 *
518
	 * @param callable $callable Callback function.
519
	 */
520
	public function each( $callable ) {
521
		foreach ( $this->elements as $element ) {
522
			call_user_func( $callable, $element );
523
		}
524
	}
525
526
	/**
527
	 * {@inheritdoc}
528
	 *
529
	 * @param callable $callable Callback function.
530
	 *
531
	 * @return Collection
532
	 */
533
	public function map( $callable ) {
534
		$elements = array();
535
		$type = null;
536
		foreach ( $this->elements as $element ) {
537
			$result = call_user_func( $callable, $element );
538
539
			if ( null === $type ) {
540
				$type = gettype( $result );
541
542
				if ( 'object' === $type ) {
543
					$type = get_class( $result );
544
				}
545
			}
546
547
			$elements[] = $result;
548
		}
549
550
		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
	public function reduce_right( $callable, $initial = null ) {
562
		return array_reduce(
563
			array_reverse( $this->elements ),
564
			$callable,
565
			$initial
566
		);
567
	}
568
569
	/**
570
	 * {@inheritdoc}
571
	 *
572
	 * @return Collection
573
	 */
574
	public function shuffle() {
575
		$elements = $this->elements;
576
		shuffle( $elements );
577
578
		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
	public function merge( $elements ) {
591
		if ( $elements instanceof static ) {
592
			$elements = $elements->to_array();
593
		}
594
595
		if ( ! is_array( $elements ) ) {
596
			throw new InvalidArgumentException( 'Merge must be given array or Collection' );
597
		}
598
599
		$this->type->validate_elements( $elements );
600
601
		return $this->new_from_trusted(
602
			array_merge( $this->elements, $elements )
603
		);
604
	}
605
606
	/**
607
	 * {@inheritdoc}
608
	 *
609
	 * @return mixed
610
	 *
611
	 * @throws OutOfBoundsException
612
	 */
613
	public function first() {
614
		if ( empty( $this->elements ) ) {
615
			throw new OutOfBoundsException( 'Cannot get first element of empty Collection' );
616
		}
617
618
		return reset( $this->elements );
619
	}
620
621
	/**
622
	 * {@inheritdoc}
623
	 *
624
	 * @return mixed
625
	 *
626
	 * @throws OutOfBoundsException
627
	 */
628
	public function last() {
629
		if ( empty( $this->elements ) ) {
630
			throw new OutOfBoundsException( 'Cannot get last element of empty Collection' );
631
		}
632
633
		return end( $this->elements );
634
	}
635
636
	/**
637
	 * {@inheritdoc}
638
	 *
639
	 * @return int
640
	 */
641
	public function count() {
642
		return count( $this->elements );
643
	}
644
645
	/**
646
	 * {@inheritDoc}
647
	 *
648
	 * @return array
649
	 */
650
	public function serialize() {
651
		return $this->map(function( $element ) {
652
			if ( $element instanceof Serializes ) {
653
				return $element->serialize();
654
			}
655
656
			return $element;
657
		} )->to_array();
658
	}
659
660
	/**
661
	 * Return the current element.
662
	 *
663
	 * @return mixed
664
	 */
665
	public function current() {
666
		return $this->at( $this->position );
667
	}
668
669
	/**
670
	 * Move forward to next element.
671
	 */
672
	public function next() {
673
		$this->position ++;
674
	}
675
676
	/**
677
	 * Return the key of the current element.
678
	 *
679
	 * @return mixed
680
	 */
681
	public function key() {
682
		return $this->position;
683
	}
684
685
	/**
686
	 * Checks if current position is valid.
687
	 *
688
	 * @return bool
689
	 */
690
	public function valid() {
691
		return isset( $this->elements[ $this->position ] );
692
	}
693
694
	/**
695
	 * Rewind the Iterator to the first element.
696
	 */
697
	public function rewind() {
698
		$this->position = 0;
699
	}
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
	protected function new_from_trusted( array $elements, $type = null ) {
711
		$collection = new static( null !== $type ? $type : $this->get_type() );
712
		$collection->set_from_trusted( $elements );
713
714
		return $collection;
715
	}
716
717
	/**
718
	 * Sets the elements without validating them.
719
	 *
720
	 * @param array $elements Pre-validated elements to set.
721
	 */
722
	protected function set_from_trusted( array $elements ) {
723
		$this->elements = $elements;
724
	}
725
726
	/**
727
	 * Number of elements true for the condition.
728
	 *
729
	 * @param callable $condition Condition to check.
730
	 * @return int
731
	 */
732
	protected function count_while_true( $condition ) {
733
		$count = 0;
734
735
		foreach ( $this->elements as $element ) {
736
			if ( ! $condition($element) ) {
737
				break;
738
			}
739
			$count++;
740
		}
741
742
		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
	protected function validate_index( $index ) {
753
		$exists = $this->index_exists( $index );
754
755
		if ( ! $exists ) {
756
			throw new OutOfRangeException( 'Index out of bounds of collection' );
757
		}
758
	}
759
}
760