Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangesListFilterFactory
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 9
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 registerFiltersFromDefinitions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 isConfigSatisfied
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 transformGroupDefinition
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 transformFilterDefinition
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 createGroup
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 registerSupersets
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 registerConflicts
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 handlePendingConflicts
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\RecentChanges;
4
5use InvalidArgumentException;
6use UnexpectedValueException;
7
8class ChangesListFilterFactory {
9    private ?string $showHidePrefix;
10
11    public function __construct( private array $config ) {
12        $this->showHidePrefix = $config['showHidePrefix'] ?? null;
13    }
14
15    /**
16     * Register filters from an array of group definitions
17     *
18     * Groups are displayed to the user in the structured UI.  However, if necessary,
19     * all of the filters in a group can be configured to only display on the
20     * unstructured UI, in which case you don't need a group title.
21     *
22     * The order of both groups and filters is significant; first is top-most priority,
23     * descending from there.
24     *
25     * @param ChangesListFilterGroupContainer $container
26     * @param array $definitions An array of associative arrays each consisting
27     *   of parameters passed to the group class, and also the following
28     *   parameters recognised by the factory:
29     *   - class: The name of the group class
30     *   - requireConfig: An associative array mapping a required config name
31     *     to its value. If a configuration item with that name and value was
32     *     not passed to the factory constructor, the group will not be
33     *     registered.
34     *   - filters: The following filter properties are recognised, in
35     *     addition to those used by the filter class constructor:
36     *     - requireConfig: As for the group
37     *     - subsets: string[] A list of filter names in the same group
38     *       which are subsets of this filter.
39     *     - conflictsWith: An associative array mapping the group name to
40     *       an associative array mapping the conflicting filter name to an
41     *       associative array of conflict options which may include
42     *       "globalKey", "forwardKey" and "backwardKey", which are passed
43     *       to ChangesListFilter::conflictsWith().
44     *     - conflictOptions: Defaults for "globalKey", "forwardKey" and
45     *       "backwardKey" in conflictsWith.
46     *     - showHideSuffix: This is prefixed with the factory config option
47     *       "showHidePrefix" and then passed to the filter as "showHide".
48     * @phan-param array<int,array{class:class-string<ChangesListFilterGroup>,filters:array}> $definitions
49     */
50    public function registerFiltersFromDefinitions(
51        ChangesListFilterGroupContainer $container,
52        array $definitions
53    ) {
54        foreach ( $definitions as $groupDefinition ) {
55            if ( !$this->isConfigSatisfied( $groupDefinition ) ) {
56                continue;
57            }
58            $this->transformGroupDefinition( $container, $groupDefinition );
59            $group = $this->createGroup( $groupDefinition );
60            $this->registerSupersets( $group, $groupDefinition );
61            $container->registerGroup( $group );
62            $this->registerConflicts( $container, $group, $groupDefinition );
63            $this->handlePendingConflicts( $container, $group );
64        }
65    }
66
67    /**
68     * Check whether any "requireConfig" values in the filter or group definition
69     * are satisfied by the currently defined config. If this returns false, the
70     * filter or group will not be registered.
71     *
72     * @param array $def Group or filter definition
73     * @return bool
74     */
75    private function isConfigSatisfied( array $def ) {
76        foreach ( $def['requireConfig'] ?? [] as $name => $value ) {
77            if ( !array_key_exists( $name, $this->config ) || $this->config[$name] !== $value ) {
78                return false;
79            }
80        }
81        return true;
82    }
83
84    private function transformGroupDefinition(
85        ChangesListFilterGroupContainer $container,
86        array &$groupDefinition
87    ) {
88        $groupDefinition['priority'] =
89            $container->fillPriority( $groupDefinition['priority'] ?? null );
90
91        $filterDefs = [];
92        foreach ( $groupDefinition['filters'] as $def ) {
93            if ( !$this->isConfigSatisfied( $def ) ) {
94                continue;
95            }
96            $def = $this->transformFilterDefinition( $def );
97            $filterDefs[] = $def;
98        }
99        $groupDefinition['filters'] = $filterDefs;
100    }
101
102    /**
103     * Transforms filter definition to prepare it for constructor.
104     *
105     * See overrides of this method as well.
106     *
107     * @param array $filterDefinition Original filter definition
108     *
109     * @return array Transformed definition
110     */
111    private function transformFilterDefinition( array $filterDefinition ) {
112        if ( $this->showHidePrefix !== null && isset( $filterDefinition['showHideSuffix'] ) ) {
113            $filterDefinition['showHide'] = $this->showHidePrefix . $filterDefinition['showHideSuffix'];
114        }
115
116        return $filterDefinition;
117    }
118
119    private function createGroup( array $groupDefinition ): ChangesListFilterGroup {
120        $className = $groupDefinition['class'];
121        unset( $groupDefinition['class'] );
122
123        $group = new $className( $groupDefinition );
124        if ( !( $group instanceof ChangesListFilterGroup ) ) {
125            throw new UnexpectedValueException(
126                "$className was expected to be an instance of ChangesListFilterGroup" );
127        }
128        return $group;
129    }
130
131    /**
132     * If any filters in this new group have subsets, register the subset.
133     * Subsets must be in the same group.
134     *
135     * @param ChangesListFilterGroup $group
136     * @param array $groupDefinition
137     */
138    private function registerSupersets( ChangesListFilterGroup $group, array $groupDefinition ) {
139        foreach ( $groupDefinition['filters'] as $def ) {
140            foreach ( $def['subsets'] ?? [] as $subsetName ) {
141                $filter = $group->getFilter( $def['name'] );
142                $subset = $group->getFilter( $subsetName );
143                if ( $filter && $subset ) {
144                    $filter->setAsSupersetOf( $subset );
145                }
146            }
147        }
148    }
149
150    /**
151     * Find filters with "conflictsWith" in their group definition. If the
152     * filter it refers to exists, register a conflict. Otherwise, register
153     * a pending conflict.
154     *
155     * @param ChangesListFilterGroupContainer $container
156     * @param ChangesListFilterGroup $group
157     * @param array $groupDefinition
158     */
159    private function registerConflicts(
160        ChangesListFilterGroupContainer $container,
161        ChangesListFilterGroup $group,
162        array $groupDefinition
163    ) {
164        foreach ( $groupDefinition['filters'] as $def ) {
165            $filter = $group->getFilter( $def['name'] );
166            foreach ( $def['conflictsWith'] ?? [] as $conflictingGroupName => $conflictingFilters ) {
167                foreach ( $conflictingFilters as $conflictingFilterName => $opts ) {
168                    '@phan-var array $opts';
169                    $opts += $def['conflictOptions'] ?? [];
170                    $missing = array_diff(
171                        [ 'globalKey', 'forwardKey', 'backwardKey' ],
172                        array_keys( $opts )
173                    );
174                    if ( $missing ) {
175                        throw new InvalidArgumentException(
176                            "The conflict option(s) " . implode( ', ', $missing ) .
177                            " must be present in either conflictsWith or conflictOptions in " .
178                            "the definition of filter {$group->getName()}/{$def['name']}"
179                        );
180                    }
181                    $conflictingGroup = $container->getGroup( $conflictingGroupName );
182                    $conflictingFilter = $conflictingGroup?->getFilter( $conflictingFilterName );
183                    if ( $conflictingFilter ) {
184                        $filter->conflictsWith(
185                            $conflictingFilter,
186                            $opts['globalKey'],
187                            $opts['forwardKey'],
188                            $opts['backwardKey']
189                        );
190                    } else {
191                        $container->addPendingConflict(
192                            $filter,
193                            $conflictingGroupName,
194                            $conflictingFilterName,
195                            $opts
196                        );
197                    }
198                }
199            }
200        }
201    }
202
203    /**
204     * Check whether there were any pending conflicts registered with this
205     * new group as the target. If so, remove them from the pending list and
206     * register the conflict.
207     *
208     * @param ChangesListFilterGroupContainer $container
209     * @param ChangesListFilterGroup $group
210     */
211    private function handlePendingConflicts(
212        ChangesListFilterGroupContainer $container,
213        ChangesListFilterGroup $group
214    ) {
215        foreach ( $group->getFilters() as $filter ) {
216            $conflicts = $container->popPendingConflicts( $group, $filter );
217            foreach ( $conflicts as [ $sourceFilter, $opts ] ) {
218                $sourceFilter->conflictsWith(
219                    $filter,
220                    $opts['globalKey'],
221                    $opts['forwardKey'],
222                    $opts['backwardKey']
223                );
224            }
225        }
226    }
227}