1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Table filterset.
19 *
20 * @package    core
21 * @category   table
22 * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26declare(strict_types=1);
27
28namespace core_table\local\filter;
29
30use Countable;
31use JsonSerializable;
32use InvalidArgumentException;
33use Iterator;
34
35/**
36 * Class representing a generic filter of any type.
37 *
38 * @package    core
39 * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
40 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
42class filter implements Countable, Iterator, JsonSerializable {
43
44    /** @var in The default filter type (ANY) */
45    const JOINTYPE_DEFAULT = 1;
46
47    /** @var int None of the following match */
48    const JOINTYPE_NONE = 0;
49
50    /** @var int Any of the following match */
51    const JOINTYPE_ANY = 1;
52
53    /** @var int All of the following match */
54    const JOINTYPE_ALL = 2;
55
56    /** @var string The name of this filter */
57    protected $name = null;
58
59    /** @var int The join type currently in use */
60    protected $jointype = self::JOINTYPE_DEFAULT;
61
62    /** @var array The list of active filter values */
63    protected $filtervalues = [];
64
65    /** @var int[] valid join types */
66    protected $jointypes = [
67        self::JOINTYPE_NONE,
68        self::JOINTYPE_ANY,
69        self::JOINTYPE_ALL,
70    ];
71
72    /** @var int The current iterator position */
73    protected $iteratorposition = null;
74
75    /**
76     * Constructor for the generic filter class.
77     *
78     * @param string $name The name of the current filter.
79     * @param int $jointype The join to use when combining the filters.
80     *                      See the JOINTYPE_ constants for further information on the field.
81     * @param mixed[] $values An array of filter objects to be applied.
82     */
83    public function __construct(string $name, ?int $jointype = null, ?array $values = null) {
84        $this->name = $name;
85
86        if ($jointype !== null) {
87            $this->set_join_type($jointype);
88        }
89
90        if (!empty($values)) {
91            foreach ($values as $value) {
92                $this->add_filter_value($value);
93            }
94        }
95    }
96
97    /**
98     * Reset the iterator position.
99     */
100    public function reset_iterator(): void {
101        $this->iteratorposition = null;
102    }
103
104    /**
105     * Return the current filter value.
106     */
107    public function current() {
108        if ($this->iteratorposition === null) {
109            $this->rewind();
110        }
111
112        if ($this->iteratorposition === null) {
113            return null;
114        }
115
116        return $this->filtervalues[$this->iteratorposition];
117    }
118
119    /**
120     * Returns the current position of the iterator.
121     *
122     * @return int
123     */
124    public function key() {
125        if ($this->iteratorposition === null) {
126            $this->rewind();
127        }
128
129        return $this->iteratorposition;
130    }
131
132    /**
133     * Rewind the Iterator position to the start.
134     */
135    public function rewind(): void {
136        if ($this->iteratorposition === null) {
137            $this->sort_filter_values();
138        }
139
140        if (count($this->filtervalues)) {
141            $this->iteratorposition = 0;
142        }
143    }
144
145    /**
146     * Move to the next value in the list.
147     */
148    public function next(): void {
149        ++$this->iteratorposition;
150    }
151
152    /**
153     * Check if the current position is valid.
154     *
155     * @return bool
156     */
157    public function valid(): bool {
158        return isset($this->filtervalues[$this->iteratorposition]);
159    }
160
161    /**
162     * Return the number of contexts.
163     *
164     * @return int
165     */
166    public function count(): int {
167        return count($this->filtervalues);
168    }
169
170    /**
171     * Return the name of the filter.
172     *
173     * @return string
174     */
175    public function get_name(): string {
176        return $this->name;
177    }
178
179    /**
180     * Specify the type of join to employ for the filter.
181     *
182     * @param int $jointype The join type to use using one of the supplied constants
183     * @return self
184     */
185    public function set_join_type(int $jointype): self {
186        if (array_search($jointype, $this->jointypes) === false) {
187            throw new InvalidArgumentException('Invalid join type specified');
188        }
189
190        $this->jointype = $jointype;
191
192        return $this;
193    }
194
195    /**
196     * Return the currently specified join type.
197     *
198     * @return int
199     */
200    public function get_join_type(): int {
201        return $this->jointype;
202    }
203
204    /**
205     * Add a value to the filter.
206     *
207     * @param mixed $value
208     * @return self
209     */
210    public function add_filter_value($value): self {
211        if ($value === null) {
212            // Null values are usually invalid.
213            return $this;
214        }
215
216        if ($value === '') {
217            // Empty strings are invalid.
218            return $this;
219        }
220
221        if (array_search($value, $this->filtervalues) !== false) {
222            // Remove duplicates.
223            return $this;
224        }
225
226        $this->filtervalues[] = $value;
227
228        // Reset the iterator position.
229        $this->reset_iterator();
230
231        return $this;
232    }
233
234    /**
235     * Sort the filter values to ensure reliable, and consistent output.
236     */
237    protected function sort_filter_values(): void {
238        // Sort the filter values to ensure consistent output.
239        // Note: This is not a locale-aware sort, but we don't need this.
240        // It's primarily for consistency, not for actual sorting.
241        sort($this->filtervalues);
242
243        $this->reset_iterator();
244    }
245
246    /**
247     * Return the current filter values.
248     *
249     * @return mixed[]
250     */
251    public function get_filter_values(): array {
252        $this->sort_filter_values();
253        return $this->filtervalues;
254    }
255
256    /**
257     * Serialize filter.
258     *
259     * @return mixed|object
260     */
261    public function jsonSerialize() {
262        return (object) [
263            'name' => $this->get_name(),
264            'jointype' => $this->get_join_type(),
265            'values' => $this->get_filter_values(),
266        ];
267    }
268}
269