Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.43% covered (warning)
71.43%
25 / 35
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
FilteredSequentialIterator
71.43% covered (warning)
71.43%
25 / 35
60.00% covered (warning)
60.00%
3 / 5
25.56
0.00% covered (danger)
0.00%
0 / 1
 add
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
13.57
 addFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 createIterator
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 createFilter
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
6.35
1<?php
2
3namespace MediaWiki\Extension\Notifications\Iterator;
4
5use ArrayIterator;
6use CallbackFilterIterator;
7use EmptyIterator;
8use InvalidArgumentException;
9use Iterator;
10use IteratorAggregate;
11use RecursiveIteratorIterator;
12
13/**
14 * Allows building a single iterator out of multiple iterators
15 * and filtering the results.  Accepts plain arrays for the simple
16 * use case, also accepts Iterator instances for anything more complex.
17 *
18 * This exists so that UserLocator implementations can return iterators
19 * that return potentially thousands of users without having to grab
20 * them all in one giant query.
21 *
22 * Usage:
23 *   $users = new FilteredSequentialIterator;
24 *   $users->add( [ $userA, $userB, $userC ] );
25 *
26 *   $it = new BatchRowIterator( ... );
27 *   ...
28 *   $it = new RecursiveIteratorIterator( $it );
29 *   $users->add( new CallbackIterator( $it, function( $row ) {
30 *    ...
31 *    return $user;
32 *   } ) );
33 *
34 *   foreach ( $users as $user ) {
35 *    ...
36 *   }
37 *
38 * By default the BatchRowIterator returns an array of rows, this class
39 * expects a stream of user objects.  To bridge that gap the
40 * RecursiveIteratorIterator is used to flatten and the CallbackIterator
41 * is used to transform each database $row into a User object.
42 *
43 * @todo name?
44 */
45class FilteredSequentialIterator implements IteratorAggregate {
46    /**
47     * @var Iterator[]
48     */
49    protected $iterators = [];
50
51    /**
52     * @var callable[]
53     */
54    protected $filters = [];
55
56    /**
57     * @param Iterator|IteratorAggregate|array $users
58     */
59    public function add( $users ) {
60        if ( is_array( $users ) ) {
61            $it = new ArrayIterator( $users );
62        } elseif ( $users instanceof Iterator ) {
63            $it = $users;
64        } elseif ( $users instanceof IteratorAggregate ) {
65            $it = $users->getIterator();
66        } else {
67            throw new InvalidArgumentException( 'Expected array, Iterator or IteratorAggregate but received:' .
68                ( is_object( $users ) ? get_class( $users ) : gettype( $users ) )
69            );
70        }
71
72        $this->iterators[] = $it;
73    }
74
75    /**
76     * @param callable $callable
77     */
78    public function addFilter( $callable ) {
79        $this->filters[] = $callable;
80    }
81
82    /**
83     * Satisfies IteratorAggregate interface
84     *
85     * @return Iterator
86     */
87    public function getIterator(): Iterator {
88        $it = $this->createIterator();
89        if ( $this->filters ) {
90            $it = new CallbackFilterIterator( $it, $this->createFilter() );
91        }
92
93        return $it;
94    }
95
96    /**
97     * @return Iterator
98     */
99    protected function createIterator() {
100        switch ( count( $this->iterators ) ) {
101            case 0:
102                return new EmptyIterator;
103
104            case 1:
105                return reset( $this->iterators );
106
107            default:
108                return new RecursiveIteratorIterator( new MultipleIterator( $this->iterators ) );
109        }
110    }
111
112    /**
113     * @return callable
114     */
115    protected function createFilter() {
116        switch ( count( $this->filters ) ) {
117            case 0:
118                return static function () {
119                    return true;
120                };
121
122            case 1:
123                return reset( $this->filters );
124
125            default:
126                $filters = $this->filters;
127
128                return static function ( $user ) use ( $filters ) {
129                    foreach ( $filters as $filter ) {
130                        if ( !call_user_func( $filter, $user ) ) {
131                            return false;
132                        }
133                    }
134
135                    return true;
136                };
137        }
138    }
139}