MDL-69145 core: Set the default filterset join type to ALL
[moodle.git] / lib / table / classes / local / filter / filterset.php
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/>.
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  */
26 declare(strict_types=1);
28 namespace core_table\local\filter;
30 use InvalidArgumentException;
31 use JsonSerializable;
32 use UnexpectedValueException;
33 use moodle_exception;
35 /**
36  * Class representing a set of filters.
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  */
42 abstract class filterset implements JsonSerializable {
43     /** @var int The default filter type (ALL) */
44     const JOINTYPE_DEFAULT = 2;
46     /** @var int None of the following match */
47     const JOINTYPE_NONE = 0;
49     /** @var int Any of the following match */
50     const JOINTYPE_ANY = 1;
52     /** @var int All of the following match */
53     const JOINTYPE_ALL = 2;
55     /** @var int The join type currently in use */
56     protected $jointype = null;
58     /** @var array The list of combined filter types */
59     protected $filtertypes = null;
61     /** @var array The list of active filters */
62     protected $filters = [];
64     /** @var int[] valid join types */
65     protected $jointypes = [
66         self::JOINTYPE_NONE,
67         self::JOINTYPE_ANY,
68         self::JOINTYPE_ALL,
69     ];
71     /**
72      * Specify the type of join to employ for the filter.
73      *
74      * @param int $jointype The join type to use using one of the supplied constants
75      * @return self
76      */
77     public function set_join_type(int $jointype): self {
78         if (array_search($jointype, $this->jointypes) === false) {
79             throw new InvalidArgumentException('Invalid join type specified');
80         }
82         $this->jointype = $jointype;
84         return $this;
85     }
87     /**
88      * Return the currently specified join type.
89      *
90      * @return int
91      */
92     public function get_join_type(): int {
93         if ($this->jointype === null) {
94             $this->jointype = self::JOINTYPE_DEFAULT;
95         }
96         return $this->jointype;
97     }
99     /**
100      * Add the specified filter.
101      *
102      * @param filter $filter
103      * @return self
104      */
105     public function add_filter(filter $filter): self {
106         $filtername = $filter->get_name();
108         if (array_key_exists($filtername, $this->filters)) {
109             // This filter already exists.
110             if ($this->filters[$filtername] === $filter) {
111                 // This is the same value as already added.
112                 // Just ignore it.
113                 return $this;
114             }
116             // This is a different value to last time. Fail as this is not supported.
117             throw new UnexpectedValueException(
118                 "A filter of type '{$filtername}' has already been added. Check that you have the correct filter."
119             );
120         }
122         // Ensure that the filter is both known, and is of the correct type.
123         $validtypes = $this->get_all_filtertypes();
125         if (!array_key_exists($filtername, $validtypes)) {
126             // Unknown filter.
127             throw new InvalidArgumentException(
128                 "The filter '{$filtername}' was not recognised."
129             );
130         }
132         // Check that the filter is of the correct type.
133         if (!is_a($filter, $validtypes[$filtername])) {
134             $actualtype = get_class($filter);
135             $requiredtype = $validtypes[$filtername];
137             throw new InvalidArgumentException(
138                 "The filter '{$filtername}' was incorrectly specified as a {$actualtype}. It must be a {$requiredtype}."
139             );
140         }
142         // All good. Add the filter.
143         $this->filters[$filtername] = $filter;
145         return $this;
146     }
148     /**
149      * Add the specified filter from the supplied params.
150      *
151      * @param string $filtername The name of the filter to create
152      * @param mixed[] ...$args Additional arguments used to create this filter type
153      * @return self
154      */
155     public function add_filter_from_params(string $filtername, ...$args): self {
156         // Fetch the list of valid filters by name.
157         $validtypes = $this->get_all_filtertypes();
159         if (!array_key_exists($filtername, $validtypes)) {
160             // Unknown filter.
161             throw new InvalidArgumentException(
162                 "The filter '{$filtername}' was not recognised."
163             );
164         }
166         $filterclass = $validtypes[$filtername];
168         if (!class_exists($filterclass)) {
169             // Filter class cannot be class autoloaded.
170             throw new InvalidArgumentException(
171                 "The filter class '{$filterclass}' for filter '{$filtername}' could not be found."
172             );
173         }
175         // Pass all supplied arguments to the constructor when adding a new filter.
176         // This allows for a wider definition of the the filter in child classes.
177         $this->add_filter(new $filterclass($filtername, ...$args));
179         return $this;
180     }
182     /**
183      * Return the current set of filters.
184      *
185      * @return filter[]
186      */
187     public function get_filters(): array {
188         // Sort the filters by their name to ensure consistent output.
189         // Note: This is not a locale-aware sort, but we don't need this.
190         // It's primarily for consistency, not for actual sorting.
191         asort($this->filters);
193         return $this->filters;
194     }
196     /**
197      * Check whether the filter has been added or not.
198      *
199      * @param string $filtername
200      * @return bool
201      */
202     public function has_filter(string $filtername): bool {
203         // We do not check if the filtername is valid, only that it exists.
204         // This is an existence check and there is no benefit to doing any more.
205         return array_key_exists($filtername, $this->filters);
206     }
208     /**
209      * Get the named filter.
210      *
211      * @param string $filtername
212      * @return filter
213      */
214     public function get_filter(string $filtername): filter {
215         if (!array_key_exists($filtername, $this->get_all_filtertypes())) {
216             throw new UnexpectedValueException("The filter specified ({$filtername}) is invalid.");
217         }
219         if (!array_key_exists($filtername, $this->filters)) {
220             throw new UnexpectedValueException("The filter specified ({$filtername}) has not been created.");
221         }
223         return $this->filters[$filtername];
224     }
226     /**
227      * Confirm whether the filter has been correctly specified.
228      *
229      * @throws moodle_exception
230      */
231     public function check_validity(): void {
232         // Ensure that all required filters are present.
233         $missing = [];
234         foreach (array_keys($this->get_required_filters()) as $filtername) {
235             if (!array_key_exists($filtername, $this->filters)) {
236                 $missing[] = $filtername;
237             }
238         }
240         if (!empty($missing)) {
241             throw new moodle_exception(
242                 'missingrequiredfields',
243                 'core_table',
244                 '',
245                 implode(get_string('listsep', 'langconfig') . ' ', $missing)
246             );
247         }
248     }
250     /**
251      * Get the list of required filters in an array of filtername => filter class type.
252      *
253      * @return array
254      */
255     protected function get_required_filters(): array {
256         return [];
257     }
259     /**
260      * Get the list of optional filters in an array of filtername => filter class type.
261      *
262      * @return array
263      */
264     protected function get_optional_filters(): array {
265         return [];
266     }
268     /**
269      * Get all filter valid types in an array of filtername => filter class type.
270      *
271      * @return array
272      */
273     public function get_all_filtertypes(): array {
274         if ($this->filtertypes === null) {
275             $required = $this->get_required_filters();
276             $optional = $this->get_optional_filters();
278             $conflicts = array_keys(array_intersect_key($required, $optional));
280             if (!empty($conflicts)) {
281                 throw new InvalidArgumentException(
282                     "Some filter types are both required, and optional: " . implode(', ', $conflicts)
283                 );
284             }
286             $this->filtertypes = array_merge($required, $optional);
287             asort($this->filtertypes);
288         }
290         return $this->filtertypes;
291     }
293     /**
294      * Serialize filterset.
295      *
296      * @return mixed|object
297      */
298     public function jsonSerialize() {
299         return (object) [
300             'jointype' => $this->get_join_type(),
301             'filters' => $this->get_filters(),
302         ];
303     }