MediaWiki  master
ChangesListFilterGroup.php
Go to the documentation of this file.
1 <?php
24 // TODO: Might want to make a super-class or trait to share behavior (especially re
25 // conflicts) between ChangesListFilter and ChangesListFilterGroup.
26 // What to call it. FilterStructure? That would also let me make
27 // setUnidirectionalConflict protected.
28 
31 
38 abstract class ChangesListFilterGroup {
44  protected $name;
45 
51  protected $title;
52 
58  protected $whatsThisHeader;
59 
65  protected $whatsThisBody;
66 
72  protected $whatsThisUrl;
73 
79  protected $whatsThisLinkText;
80 
86  protected $type;
87 
94  protected $priority;
95 
101  protected $filters;
102 
109  protected $isFullCoverage;
110 
117  protected $conflictingGroups = [];
118 
125  protected $conflictingFilters = [];
126 
127  private const DEFAULT_PRIORITY = -100;
128 
129  private const RESERVED_NAME_CHAR = '_';
130 
157  public function __construct( array $groupDefinition ) {
158  if ( strpos( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
159  throw new InvalidArgumentException( 'Group names may not contain \'' .
160  self::RESERVED_NAME_CHAR .
161  '\'. Use the naming convention: \'camelCase\''
162  );
163  }
164 
165  $this->name = $groupDefinition['name'];
166 
167  if ( isset( $groupDefinition['title'] ) ) {
168  $this->title = $groupDefinition['title'];
169  }
170 
171  if ( isset( $groupDefinition['whatsThisHeader'] ) ) {
172  $this->whatsThisHeader = $groupDefinition['whatsThisHeader'];
173  $this->whatsThisBody = $groupDefinition['whatsThisBody'];
174  $this->whatsThisUrl = $groupDefinition['whatsThisUrl'];
175  $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText'];
176  }
177 
178  $this->type = $groupDefinition['type'];
179  $this->priority = $groupDefinition['priority'] ?? self::DEFAULT_PRIORITY;
180 
181  $this->isFullCoverage = $groupDefinition['isFullCoverage'];
182 
183  $this->filters = [];
184  $lowestSpecifiedPriority = -1;
185  foreach ( $groupDefinition['filters'] as $filterDefinition ) {
186  if ( isset( $filterDefinition['priority'] ) ) {
187  $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
188  }
189  }
190 
191  // Convenience feature: If you specify a group (and its filters) all in
192  // one place, you don't have to specify priority. You can just put them
193  // in order. However, if you later add one (e.g. an extension adds a filter
194  // to a core-defined group), you need to specify it.
195  $autoFillPriority = $lowestSpecifiedPriority - 1;
196  foreach ( $groupDefinition['filters'] as $filterDefinition ) {
197  if ( !isset( $filterDefinition['priority'] ) ) {
198  $filterDefinition['priority'] = $autoFillPriority;
199  $autoFillPriority--;
200  }
201  $filterDefinition['group'] = $this;
202 
203  $filter = $this->createFilter( $filterDefinition );
204  $this->registerFilter( $filter );
205  }
206  }
207 
214  abstract protected function createFilter( array $filterDefinition );
215 
230  public function conflictsWith( $other, string $globalKey, string $forwardKey, string $backwardKey ) {
232  $other,
233  $globalKey,
234  $forwardKey
235  );
236 
237  $other->setUnidirectionalConflict(
238  $this,
239  $globalKey,
240  $backwardKey
241  );
242  }
243 
255  public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
256  if ( $other instanceof ChangesListFilterGroup ) {
257  $this->conflictingGroups[] = [
258  'group' => $other->getName(),
259  'groupObject' => $other,
260  'globalDescription' => $globalDescription,
261  'contextDescription' => $contextDescription,
262  ];
263  } elseif ( $other instanceof ChangesListFilter ) {
264  $this->conflictingFilters[] = [
265  'group' => $other->getGroup()->getName(),
266  'filter' => $other->getName(),
267  'filterObject' => $other,
268  'globalDescription' => $globalDescription,
269  'contextDescription' => $contextDescription,
270  ];
271  } else {
272  throw new InvalidArgumentException(
273  'You can only pass in a ChangesListFilterGroup or a ChangesListFilter'
274  );
275  }
276  }
277 
281  public function getName() {
282  return $this->name;
283  }
284 
288  public function getTitle() {
289  return $this->title;
290  }
291 
295  public function getType() {
296  return $this->type;
297  }
298 
302  public function getPriority() {
303  return $this->priority;
304  }
305 
310  public function getFilters() {
311  return $this->filters;
312  }
313 
320  public function getFilter( $name ) {
321  return $this->filters[$name] ?? null;
322  }
323 
331  public function getJsData() {
332  $output = [
333  'name' => $this->name,
334  'type' => $this->type,
335  'fullCoverage' => $this->isFullCoverage,
336  'filters' => [],
337  'priority' => $this->priority,
338  'conflicts' => [],
339  'messageKeys' => [ $this->title ]
340  ];
341 
342  if ( isset( $this->whatsThisHeader ) ) {
343  $output['whatsThisHeader'] = $this->whatsThisHeader;
344  $output['whatsThisBody'] = $this->whatsThisBody;
345  $output['whatsThisUrl'] = $this->whatsThisUrl;
346  $output['whatsThisLinkText'] = $this->whatsThisLinkText;
347 
348  array_push(
349  $output['messageKeys'],
350  $output['whatsThisHeader'],
351  $output['whatsThisBody'],
352  $output['whatsThisLinkText']
353  );
354  }
355 
356  usort( $this->filters, static function ( ChangesListFilter $a, ChangesListFilter $b ) {
357  return $b->getPriority() <=> $a->getPriority();
358  } );
359 
360  foreach ( $this->filters as $filter ) {
361  if ( $filter->displaysOnStructuredUi() ) {
362  $filterData = $filter->getJsData();
363  $output['messageKeys'] = array_merge(
364  $output['messageKeys'],
365  $filterData['messageKeys']
366  );
367  unset( $filterData['messageKeys'] );
368  $output['filters'][] = $filterData;
369  }
370  }
371 
372  if ( count( $output['filters'] ) === 0 ) {
373  return null;
374  }
375 
376  $output['title'] = $this->title;
377 
378  $conflicts = array_merge(
379  $this->conflictingGroups,
380  $this->conflictingFilters
381  );
382 
383  foreach ( $conflicts as $conflictInfo ) {
384  unset( $conflictInfo['filterObject'] );
385  unset( $conflictInfo['groupObject'] );
386  $output['conflicts'][] = $conflictInfo;
387  array_push(
388  $output['messageKeys'],
389  $conflictInfo['globalDescription'],
390  $conflictInfo['contextDescription']
391  );
392  }
393 
394  return $output;
395  }
396 
402  public function getConflictingGroups() {
403  return array_column( $this->conflictingGroups, 'groupObject' );
404  }
405 
411  public function getConflictingFilters() {
412  return array_column( $this->conflictingFilters, 'filterObject' );
413  }
414 
421  public function anySelected( FormOptions $opts ) {
422  return (bool)count( array_filter(
423  $this->getFilters(),
424  static function ( ChangesListFilter $filter ) use ( $opts ) {
425  return $filter->isSelected( $opts );
426  }
427  ) );
428  }
429 
446  abstract public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage,
447  &$tables, &$fields, &$conds, &$query_options, &$join_conds,
448  FormOptions $opts, $isStructuredFiltersEnabled );
449 
457  abstract public function addOptions( FormOptions $opts, $allowDefaults,
458  $isStructuredFiltersEnabled );
459 }
Represents a filter group (used on ChangesListSpecialPage and descendants)
int $priority
Priority integer.
string $name
Name (internal identifier)
string null $whatsThisBody
i18n key for body of What's This?
string $title
i18n key for title
anySelected(FormOptions $opts)
Check if any filter in this group is selected.
string null $whatsThisUrl
URL of What's This? link.
bool $isFullCoverage
Whether this group is full coverage.
modifyQuery(IDatabase $dbr, ChangesListSpecialPage $specialPage, &$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts, $isStructuredFiltersEnabled)
Modifies the query to include the filter group.
getFilter( $name)
Get filter by name.
ChangesListFilter[] $filters
Associative array of filters, as ChangesListFilter objects, with filter name as key.
conflictsWith( $other, string $globalKey, string $forwardKey, string $backwardKey)
Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
getConflictingGroups()
Get groups conflicting with this filter group.
array $conflictingFilters
Array of associative arrays with conflict information.
getJsData()
Gets the JS data in the format required by the front-end of the structured UI.
__construct(array $groupDefinition)
Create a new filter group with the specified configuration.
string null $whatsThisHeader
i18n key for header of What's This?
addOptions(FormOptions $opts, $allowDefaults, $isStructuredFiltersEnabled)
All the options represented by this filter group to $opts.
getConflictingFilters()
Get filters conflicting with this filter group.
array $conflictingGroups
Array of associative arrays with conflict information.
setUnidirectionalConflict( $other, $globalDescription, $contextDescription)
Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
createFilter(array $filterDefinition)
Creates a filter of the appropriate type for this group, from the definition.
string $type
Type, from a TYPE constant of a subclass.
string null $whatsThisLinkText
i18n key for What's This? link
Represents a filter (used on ChangesListSpecialPage and descendants)
isSelected(FormOptions $opts)
Checks whether this filter is selected in the provided options.
Special page which uses a ChangesList to show query results.
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:41
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36