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