MediaWiki master
ChangesListFilterGroup.php
Go to the documentation of this file.
1<?php
8
9use InvalidArgumentException;
14
28abstract class ChangesListFilterGroup {
34 protected $name;
35
41 protected $title;
42
49
55 protected $whatsThisBody;
56
62 protected $whatsThisUrl;
63
70
76 protected $type;
77
84 protected $priority;
85
91 protected $filters;
92
99 protected $isFullCoverage;
100
107 protected $conflictingGroups = [];
108
115 protected $conflictingFilters = [];
116
117 private const DEFAULT_PRIORITY = -100;
118
119 private const RESERVED_NAME_CHAR = '_';
120
147 public function __construct( array $groupDefinition ) {
148 if ( str_contains( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) ) {
149 throw new InvalidArgumentException( 'Group names may not contain \'' .
150 self::RESERVED_NAME_CHAR .
151 '\'. Use the naming convention: \'camelCase\''
152 );
153 }
154
155 $this->name = $groupDefinition['name'];
156
157 if ( isset( $groupDefinition['title'] ) ) {
158 $this->title = $groupDefinition['title'];
159 }
160
161 if ( isset( $groupDefinition['whatsThisHeader'] ) ) {
162 $this->whatsThisHeader = $groupDefinition['whatsThisHeader'];
163 $this->whatsThisBody = $groupDefinition['whatsThisBody'];
164 $this->whatsThisUrl = $groupDefinition['whatsThisUrl'];
165 $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText'];
166 }
167
168 $this->type = $groupDefinition['type'];
169 $this->priority = $groupDefinition['priority'] ?? self::DEFAULT_PRIORITY;
170
171 $this->isFullCoverage = $groupDefinition['isFullCoverage'];
172
173 $this->filters = [];
174 $lowestSpecifiedPriority = -1;
175 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
176 if ( isset( $filterDefinition['priority'] ) ) {
177 $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
178 }
179 }
180
181 // Convenience feature: If you specify a group (and its filters) all in
182 // one place, you don't have to specify priority. You can just put them
183 // in order. However, if you later add one (e.g. an extension adds a filter
184 // to a core-defined group), you need to specify it.
185 $autoFillPriority = $lowestSpecifiedPriority - 1;
186 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
187 if ( !isset( $filterDefinition['priority'] ) ) {
188 $filterDefinition['priority'] = $autoFillPriority;
189 $autoFillPriority--;
190 }
191 $filterDefinition['group'] = $this;
192
193 $filter = $this->createFilter( $filterDefinition );
194 $this->registerFilter( $filter );
195 }
196 }
197
204 abstract protected function createFilter( array $filterDefinition );
205
212 abstract public function setDefault( $defaultValue );
213
228 public function conflictsWith( $other, string $globalKey, string $forwardKey, string $backwardKey ) {
230 $other,
231 $globalKey,
232 $forwardKey
233 );
234
235 $other->setUnidirectionalConflict(
236 $this,
237 $globalKey,
238 $backwardKey
239 );
240 }
241
253 public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
254 if ( $other instanceof ChangesListFilterGroup ) {
255 $this->conflictingGroups[] = [
256 'group' => $other->getName(),
257 'groupObject' => $other,
258 'globalDescription' => $globalDescription,
259 'contextDescription' => $contextDescription,
260 ];
261 } elseif ( $other instanceof ChangesListFilter ) {
262 $this->conflictingFilters[] = [
263 'group' => $other->getGroup()->getName(),
264 'filter' => $other->getName(),
265 'filterObject' => $other,
266 'globalDescription' => $globalDescription,
267 'contextDescription' => $contextDescription,
268 ];
269 } else {
270 throw new InvalidArgumentException(
271 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter'
272 );
273 }
274 }
275
279 public function getName() {
280 return $this->name;
281 }
282
286 public function getTitle() {
287 return $this->title;
288 }
289
293 public function getType() {
294 return $this->type;
295 }
296
300 public function getPriority() {
301 return $this->priority;
302 }
303
308 public function getFilters() {
309 return $this->filters;
310 }
311
318 public function getFilter( $name ) {
319 return $this->filters[$name] ?? null;
320 }
321
329 public function getJsData() {
330 $output = [
331 'name' => $this->name,
332 'type' => $this->type,
333 'fullCoverage' => $this->isFullCoverage,
334 'filters' => [],
335 'priority' => $this->priority,
336 'conflicts' => [],
337 'messageKeys' => [ $this->title ]
338 ];
339
340 if ( $this->whatsThisHeader !== null ) {
341 $output['whatsThisHeader'] = $this->whatsThisHeader;
342 $output['whatsThisBody'] = $this->whatsThisBody;
343 $output['whatsThisUrl'] = $this->whatsThisUrl;
344 $output['whatsThisLinkText'] = $this->whatsThisLinkText;
345
346 array_push(
347 $output['messageKeys'],
348 $output['whatsThisHeader'],
349 $output['whatsThisBody'],
350 $output['whatsThisLinkText']
351 );
352 }
353
354 usort( $this->filters, static function ( ChangesListFilter $a, ChangesListFilter $b ) {
355 return $b->getPriority() <=> $a->getPriority();
356 } );
357
358 foreach ( $this->filters as $filter ) {
359 if ( $filter->displaysOnStructuredUi() ) {
360 $filterData = $filter->getJsData();
361 $output['messageKeys'] = array_merge(
362 $output['messageKeys'],
363 $filterData['messageKeys']
364 );
365 unset( $filterData['messageKeys'] );
366 $output['filters'][] = $filterData;
367 }
368 }
369
370 if ( count( $output['filters'] ) === 0 ) {
371 return null;
372 }
373
374 $output['title'] = $this->title;
375
376 $conflicts = array_merge(
377 $this->conflictingGroups,
378 $this->conflictingFilters
379 );
380
381 foreach ( $conflicts as $conflictInfo ) {
382 unset( $conflictInfo['filterObject'] );
383 unset( $conflictInfo['groupObject'] );
384 $output['conflicts'][] = $conflictInfo;
385 array_push(
386 $output['messageKeys'],
387 $conflictInfo['globalDescription'],
388 $conflictInfo['contextDescription']
389 );
390 }
391
392 return $output;
393 }
394
400 public function getConflictingGroups() {
401 return array_column( $this->conflictingGroups, 'groupObject' );
402 }
403
409 public function getConflictingFilters() {
410 return array_column( $this->conflictingFilters, 'filterObject' );
411 }
412
419 public function anySelected( FormOptions $opts ) {
420 return (bool)count( array_filter(
421 $this->getFilters(),
422 static function ( ChangesListFilter $filter ) use ( $opts ) {
423 return $filter->isSelected( $opts );
424 }
425 ) );
426 }
427
444 public function modifyQuery( IReadableDatabase $dbr, ChangesListSpecialPage $specialPage,
445 &$tables, &$fields, &$conds, &$query_options, &$join_conds,
446 FormOptions $opts, $isStructuredFiltersEnabled
447 ) {
448 }
449
457 public function modifyChangesListQuery(
458 ChangesListQuery $query,
459 FormOptions $opts,
460 $isStructuredFiltersEnabled
461 ) {
462 foreach ( $this->getFilters() as $filter ) {
463 $action = $filter->getAction();
464 if ( $action !== null ) {
465 if ( $filter->isActive( $opts, $isStructuredFiltersEnabled ) ) {
466 if ( is_array( $action[0] ) ) {
467 foreach ( $action as $singleAction ) {
468 // @phan-suppress-next-line PhanParamTooFewUnpack
469 $query->applyAction( ...$singleAction );
470 }
471 } else {
472 // @phan-suppress-next-line PhanParamTooFewUnpack
473 $query->applyAction( ...$action );
474 }
475 }
476 $highlightAction = $filter->getHighlightAction();
477 if ( $filter->getCssClass() !== null && $highlightAction ) {
478 $name = $this->getName() . '/' . $filter->getName();
479 if ( is_array( $highlightAction[0] ) ) {
480 foreach ( $highlightAction as $singleAction ) {
481 // @phan-suppress-next-line PhanParamTooFewUnpack
482 $query->highlight( $name, ...$singleAction );
483 }
484 } else {
485 // @phan-suppress-next-line PhanParamTooFewUnpack
486 $query->highlight( $name, ...$highlightAction );
487 }
488 }
489 }
490 }
491 }
492
500 abstract public function addOptions( FormOptions $opts, $allowDefaults,
501 $isStructuredFiltersEnabled );
502}
503
505class_alias( ChangesListFilterGroup::class, 'ChangesListFilterGroup' );
Helper class to keep track of options when mixing links and form elements.
Represents a filter group (used on ChangesListSpecialPage and descendants)
string null $whatsThisBody
i18n key for body of What's This?
setUnidirectionalConflict( $other, $globalDescription, $contextDescription)
Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
addOptions(FormOptions $opts, $allowDefaults, $isStructuredFiltersEnabled)
Add all the options represented by this filter group to $opts.
modifyChangesListQuery(ChangesListQuery $query, FormOptions $opts, $isStructuredFiltersEnabled)
Modifies the query to include the filter group.
getConflictingGroups()
Get groups conflicting with this filter group.
array $conflictingGroups
Array of associative arrays with conflict information.
getConflictingFilters()
Get filters conflicting with this filter group.
conflictsWith( $other, string $globalKey, string $forwardKey, string $backwardKey)
Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
getJsData()
Gets the JS data in the format required by the front-end of the structured UI.
string null $whatsThisLinkText
i18n key for What's This? link
string null $whatsThisUrl
URL of What's This? link.
anySelected(FormOptions $opts)
Check if any filter in this group is selected.
string $type
Type, from a TYPE constant of a subclass.
string null $whatsThisHeader
i18n key for header of What's This?
array $conflictingFilters
Array of associative arrays with conflict information.
setDefault( $defaultValue)
Set the default for this filter group.
ChangesListFilter[] $filters
Associative array of filters, as ChangesListFilter objects, with filter name as key.
__construct(array $groupDefinition)
Create a new filter group with the specified configuration.
bool $isFullCoverage
Whether this group is full coverage.
modifyQuery(IReadableDatabase $dbr, ChangesListSpecialPage $specialPage, &$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts, $isStructuredFiltersEnabled)
Modifies the query to include the filter group (legacy interface).
createFilter(array $filterDefinition)
Creates a filter of the appropriate type for this group, from the definition.
Represents a filter (used on ChangesListSpecialPage and descendants)
isSelected(FormOptions $opts)
Checks whether this filter is selected in the provided options.
Build and execute a query on the recentchanges table, optionally with joins and conditions.
highlight(string $name, string $verb, string $moduleName, $value=null)
Add a highlight to the query.
applyAction(string $verb, string $moduleName, $value=null)
Apply an arbitrary action.
Special page which uses a ChangesList to show query results.
A database connection without write operations.