Completed
Pull Request — master (#15)
by James
03:38
created

Collection::find_last()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
crap 6
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 57
	public function __construct( $type, array $elements = array() ) {
46 57
		$this->type = new Type( $type );
47
48 57
		if ( $this->type->is_model() ) {
49 12
			foreach ( $elements as $idx => $element ) {
50 9
				if ( is_array( $element ) ) {
51 9
					$elements[ $idx ] = $this->type->create_model( $element );
52 9
				}
53 12
			}
54 12
		}
55
56 57
		if ( $elements ) {
57 24
			$this->type->validate_elements( $elements );
58 24
		}
59
60 57
		$this->elements = $elements;
61 57
	}
62
63
	/**
64
	 * {@inheritdoc}
65
	 *
66
	 * @return string
67
	 */
68 12
	public function get_type() {
69 12
		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 12
	public function add( $element ) {
82 12
		if ( $this->type->is_model() && is_array( $element ) ) {
83 3
			$element = $this->type->create_model( $element );
84 3
		}
85
86 12
		$this->type->validate_element( $element );
87
88 9
		$elements   = $this->elements;
89 9
		$elements[] = $element;
90
91 9
		$collection = new static( $this->get_type() );
92 9
		$collection->set_from_trusted( $elements );
93
94 9
		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( callable $condition ) {
114
		return (bool) $this->find( $condition );
115
	}
116
117
	/**
118
	 * {@inheritdoc}
119
	 *
120
	 * @param  callable $condition Condition to satisfy.
121
	 *
122
	 * @return mixed
123
	 */
124
	public function find( callable $condition ) {
125
		$index = $this->find_index( $condition );
126
127
		return -1 === $index ? false : $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( callable $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 12
	public function at( $index ) {
160 12
		$this->validate_index( $index );
161
162 12
		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 12
	public function index_exists( $index ) {
175 12
		if ( ! is_int( $index ) ) {
176
			throw new InvalidArgumentException( 'Index must be an integer' );
177
		}
178
179 12
		if ( $index < 0 ) {
180
			throw new InvalidArgumentException( 'Index must be a non-negative integer' );
181
		}
182
183 12
		return $index < $this->count();
184
	}
185
186
	/**
187
	 * {@inheritdoc}
188
	 *
189
	 * @param  callable $condition Condition to satisfy.
190
	 *
191
	 * @return mixed
192
	 */
193
	public function filter( callable $condition ) {
194
		$elements = array();
195
196
		foreach ( $this->elements as $element ) {
197
			if ( call_user_func( $condition, $element ) ) {
198
				$elements[] = $element;
199
			}
200
		}
201
202
		return $this->new_from_trusted( $elements );
203
	}
204
	/**
205
	 * {@inheritdoc}
206
	 *
207
	 * @param  callable $condition Condition to satisfy.
208
	 *
209
	 * @return mixed
210
	 */
211
	public function find_last( callable $condition ) {
212
		$index = $this->find_last_index( $condition );
213
214
		return -1 === $index ? null : $this->elements[ $index ];
215
	}
216
217
	/**
218
	 * {@inheritdoc}
219
	 *
220
	 * @param  callable $condition
221
	 * @return int
222
	 */
223
	public function find_last_index( callable $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
				break;
230
			}
231
		}
232
233
		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
	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
		$subset = array_slice( $this->elements, $start, $length );
265
266
		$collection = new static($this->type);
0 ignored issues
show
Documentation introduced by
$this->type is of type object<Intraxia\Jaxion\Axolotl\Type>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
267
		$collection->set_from_trusted( $subset );
268
269
		return $collection;
270
	}
271
272
	/**
273
	 * {@inheritdoc}
274
	 *
275
	 * @param int   $index     Index to start at.
276
	 * @param mixed $element Element to insert.
277
	 *
278
	 * @return Collection
279
	 *
280
	 * @throws InvalidArgumentException
281
	 * @throws OutOfRangeException
282
	 */
283
	public function insert( $index, $element ) {
284
		$this->validate_index( $index );
285
		$this->type->validate_element( $element );
286
287
		$a = array_slice( $this->elements, 0, $index );
288
		$b = array_slice( $this->elements, $index, count( $this->elements ) );
289
290
		$a[] = $element;
291
292
		return $this->new_from_trusted( array_merge( $a, $b ) );
293
	}
294
295
	/**
296
	 * {@inheritdoc}
297
	 *
298
	 * @param int   $index    Index to start insertion at.
299
	 * @param array $elements Elements in insert.
300
	 *
301
	 * @return Collection
302
	 *
303
	 * @throws OutOfRangeException
304
	 */
305
	public function insert_range( $index, array $elements ) {
306
		$this->validate_index( $index );
307
		$this->type->validate_elements( $elements );
308
309
		// To work with negative index, get the positive relation to 0 index
310
		$index < 0 && $index = $this->count() + $index + 1;
311
312
		$partA = array_slice( $this->elements, 0, $index );
313
		$partB = array_slice( $this->elements, $index, count( $this->elements ) );
314
315
		$elements1 = array_merge( $partA, $elements );
316
		$elements1 = array_merge( $elements1, $partB );
317
318
		$col = new static( $this->type );
0 ignored issues
show
Documentation introduced by
$this->type is of type object<Intraxia\Jaxion\Axolotl\Type>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
319
		$col->set_from_trusted( $elements1 );
320
321
		return $col;
322
	}
323
324
	/**
325
	 * {@inheritdoc}
326
	 *
327
	 * @param  callable $condition Condition to satisfy.
328
	 *
329
	 * @return Collection
330
	 */
331
	public function without( callable $condition ) {
332
		$inverse = function ( $element ) use ( $condition ) {
333
			return ! call_user_func( $condition, $element );
334
		};
335
336
		return $this->filter( $inverse );
337
	}
338
339
	/**
340
	 * {@inheritdoc}
341
	 *
342
	 * @param  int $index Index to remove.
343
	 *
344
	 * @return Collection
345
	 *
346
	 * @throws OutOfRangeException
347
	 */
348
	public function remove_at( $index ) {
349
		$this->validate_index( $index );
350
351
		$elements = $this->elements;
352
353
		return $this->new_from_trusted(
354
			array_merge(
355
				array_slice( $elements, 0, $index ),
356
				array_slice( $elements, $index + 1, count( $elements ) )
357
			)
358
		);
359
	}
360
	/**
361
	 * {@inheritdoc}
362
	 *
363
	 * @return Collection
364
	 */
365
	public function reverse() {
366
		return $this->new_from_trusted(
367
			array_reverse( $this->elements )
368
		);
369
	}
370
371
	/**
372
	 * {@inheritdoc}
373
	 *
374
	 * @param callable $callback Sort callback.
375
	 *
376
	 * @return Collection
377
	 */
378
	public function sort( callable $callback ) {
379
		$elements = $this->elements;
380
		usort( $elements, $callback );
381
		return $this->new_from_trusted( $elements );
382
	}
383
384
	/**
385
	 * {@inheritdoc}
386
	 *
387
	 * @return array
388
	 */
389 9
	public function to_array() {
390 9
		return $this->elements;
391
	}
392
393
	/**
394
	 * {@inheritdoc}
395
	 *
396
	 * @param callable $callable Reducer function.
397
	 *
398
	 * @param null     $initial  Initial reducer value.
399
	 *
400
	 * @return mixed
401
	 */
402
	public function reduce( callable $callable, $initial = null ) {
403
		return array_reduce( $this->elements, $callable, $initial );
404
	}
405
406
	/**
407
	 * {@inheritdoc}
408
	 *
409
	 * @param callable $condition Condition callback.
410
	 *
411
	 * @return bool
412
	 */
413
	public function every( callable $condition ) {
414
		$response = true;
415
416
		foreach ( $this->elements as $element ) {
417
			$result = call_user_func( $condition, $element );
418
419
			if ( false === $result ) {
420
				$response = false;
421
				break;
422
			}
423
		}
424
425
		return $response;
426
	}
427
428
	/**
429
	 * {@inheritdoc}
430
	 *
431
	 * @param  int $num Number of elements to drop.
432
	 *
433
	 * @return Collection
434
	 *
435
	 * @throws InvalidArgumentException
436
	 */
437
	public function drop( $num ) {
438
		return $this->slice( $num, $this->count() );
439
	}
440
441
	/**
442
	 * {@inheritdoc}
443
	 *
444
	 * @param int $num Number of elements to drop.
445
	 *
446
	 * @return Collection
447
	 *
448
	 * @throws InvalidArgumentException
449
	 */
450
	public function drop_right( $num ) {
451
		return $num !== $this->count()
452
			? $this->slice( 0, $this->count() - $num - 1 )
453
			: $this->clear();
454
	}
455
456
	/**
457
	 * {@inheritdoc}
458
	 *
459
	 * @param callable $condition Condition callback.
460
	 *
461
	 * @return Collection
462
	 */
463
	public function drop_while( callable $condition ) {
464
		$count = $this->count_while_true( $condition );
465
		return $count ? $this->drop( $count ) : $this;
466
	}
467
	/**
468
	 * {@inheritdoc}
469
	 *
470
	 * @return Collection
471
	 *
472
	 * @throws InvalidArgumentException
473
	 */
474
	public function tail() {
475
		return $this->slice( 1, $this->count() );
476
	}
477
478
	/**
479
	 * {@inheritdoc}
480
	 *
481
	 * @param  int $num Number of elements to take.
482
	 *
483
	 * @return Collection
484
	 *
485
	 * @throws InvalidArgumentException
486
	 */
487
	public function take( $num ) {
488
		return $this->slice( 0, $num - 1 );
489
	}
490
491
	/**
492
	 * {@inheritdoc}
493
	 *
494
	 * @param int $num Number of elements to take.
495
	 *
496
	 * @return Collection
497
	 *
498
	 * @throws InvalidArgumentException
499
	 */
500
	public function take_right( $num ) {
501
		return $this->slice( $this->count() - $num, $this->count() );
502
	}
503
504
	/**
505
	 * {@inheritdoc}
506
	 *
507
	 * @param callable $condition Callback function.
508
	 *
509
	 * @return Collection
510
	 */
511
	public function take_while( callable $condition ) {
512
		$count = $this->count_while_true( $condition );
513
514
		return $count ? $this->take( $count ) : $this->clear();
515
	}
516
517
	/**
518
	 * {@inheritdoc}
519
	 *
520
	 * @param callable $callable Callback function.
521
	 */
522
	public function each( callable $callable ) {
523
		foreach ( $this->elements as $element ) {
524
			call_user_func( $callable, $element );
525
		}
526
	}
527
528
	/**
529
	 * {@inheritdoc}
530
	 *
531
	 * @param callable $callable Callback function.
532
	 *
533
	 * @return Collection
534
	 */
535 9
	public function map( callable $callable ) {
536 9
		$elements = array();
537 9
		$type = null;
538 9
		foreach ( $this->elements as $element ) {
539 6
			$result = call_user_func( $callable, $element );
540
541 6
			if ( null === $type ) {
542 6
				$type = gettype( $result );
543
544 6
				if ( 'object' === $type ) {
545
					$type = get_class( $result );
546
				}
547 6
			}
548
549 6
			$elements[] = $result;
550 9
		}
551
552 9
		return $this->new_from_trusted( $elements, $type ? : $this->get_type() );
553
	}
554
555
	/**
556
	 * {@inheritdoc}
557
	 *
558
	 * @param callable $callable Reducer function.
559
	 * @param null     $initial  Initial value.
560
	 *
561
	 * @return mixed
562
	 */
563
	public function reduce_right( callable $callable, $initial = null ) {
564
		return array_reduce(
565
			array_reverse( $this->elements ),
566
			$callable,
567
			$initial
568
		);
569
	}
570
571
	/**
572
	 * {@inheritdoc}
573
	 *
574
	 * @return Collection
575
	 */
576
	public function shuffle() {
577
		$elements = $this->elements;
578
		shuffle( $elements );
579
580
		return $this->new_from_trusted( $elements );
581
	}
582
583
	/**
584
	 * {@inheritdoc}
585
	 *
586
	 * @param array|Collection $elements Array of elements to merge.
587
	 *
588
	 * @return Collection
589
	 *
590
	 * @throws InvalidArgumentException
591
	 */
592
	public function merge( $elements ) {
593
		if ( $elements instanceof static ) {
594
			$elements = $elements->to_array();
595
		}
596
597
		if ( ! is_array( $elements ) ) {
598
			throw new InvalidArgumentException( 'Merge must be given array or Collection' );
599
		}
600
601
		$this->type->validate_elements( $elements );
602
603
		return $this->new_from_trusted(
604
			array_merge( $this->elements, $elements )
605
		);
606
	}
607
608
	/**
609
	 * {@inheritdoc}
610
	 *
611
	 * @return mixed
612
	 *
613
	 * @throws OutOfBoundsException
614
	 */
615
	public function first() {
616
		if ( empty( $this->elements ) ) {
617
			throw new OutOfBoundsException( 'Cannot get first element of empty Collection' );
618
		}
619
620
		return reset( $this->elements );
621
	}
622
623
	/**
624
	 * {@inheritdoc}
625
	 *
626
	 * @return mixed
627
	 *
628
	 * @throws OutOfBoundsException
629
	 */
630
	public function last() {
631
		if ( empty( $this->elements ) ) {
632
			throw new OutOfBoundsException( 'Cannot get last element of empty Collection' );
633
		}
634
635
		return end( $this->elements );
636
	}
637
638
	/**
639
	 * {@inheritdoc}
640
	 *
641
	 * @return int
642
	 */
643 18
	public function count() {
644 18
		return count( $this->elements );
645
	}
646
647
	/**
648
	 * {@inheritDoc}
649
	 *
650
	 * @return array
651
	 */
652
	public function serialize() {
653 9
		return $this->map(function( $element ) {
654 6
			if ( $element instanceof Serializes ) {
655 3
				return $element->serialize();
656
			}
657
658 3
			return $element;
659 9
		} )->to_array();
660
	}
661
662
	/**
663
	 * Return the current element.
664
	 *
665
	 * @return mixed
666
	 */
667 3
	public function current() {
668 3
		return $this->at( $this->position );
669
	}
670
671
	/**
672
	 * Move forward to next element.
673
	 */
674 3
	public function next() {
675 3
		$this->position ++;
676 3
	}
677
678
	/**
679
	 * Return the key of the current element.
680
	 *
681
	 * @return mixed
682
	 */
683 3
	public function key() {
684 3
		return $this->position;
685
	}
686
687
	/**
688
	 * Checks if current position is valid.
689
	 *
690
	 * @return bool
691
	 */
692 3
	public function valid() {
693 3
		return isset( $this->elements[ $this->position ] );
694
	}
695
696
	/**
697
	 * Rewind the Iterator to the first element.
698
	 */
699 3
	public function rewind() {
700 3
		$this->position = 0;
701 3
	}
702
703
	/**
704
	 * Creates a new instance of the Collection
705
	 * from a trusted set of elements.
706
	 *
707
	 * @param array      $elements Array of elements to pass into new collection.
708
	 * @param null|mixed $type
709
	 *
710
	 * @return static
711
	 */
712 9
	protected function new_from_trusted( array $elements, $type = null ) {
713 9
		$collection = new static( null !== $type ? $type : $this->get_type() );
714 9
		$collection->set_from_trusted( $elements );
715
716 9
		return $collection;
717
	}
718
719
	/**
720
	 * Sets the elements without validating them.
721
	 *
722
	 * @param array $elements Pre-validated elements to set.
723
	 */
724 18
	protected function set_from_trusted( array $elements ) {
725 18
		$this->elements = $elements;
726 18
	}
727
728
	/**
729
	 * Number of elements true for the condition.
730
	 *
731
	 * @param callable $condition Condition to check.
732
	 * @return int
733
	 */
734
	protected function count_while_true( callable $condition ) {
735
		$count = 0;
736
737
		foreach ( $this->elements as $element ) {
738
			if ( ! $condition($element) ) {
739
				break;
740
			}
741
			$count++;
742
		}
743
744
		return $count;
745
	}
746
747
	/**
748
	 * Validates a number to be used as an index.
749
	 *
750
	 * @param  integer $index The number to be validated as an index.
751
	 *
752
	 * @throws OutOfRangeException
753
	 */
754 12
	protected function validate_index( $index ) {
755 12
		$exists = $this->index_exists( $index );
756
757 12
		if ( ! $exists ) {
758
			throw new OutOfRangeException( 'Index out of bounds of collection' );
759
		}
760 12
	}
761
}
762