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 
32 
39 abstract class ChangesListFilterGroup {
45  protected $name;
46 
52  protected $title;
53 
59  protected $whatsThisHeader;
60 
66  protected $whatsThisBody;
67 
73  protected $whatsThisUrl;
74 
80  protected $whatsThisLinkText;
81 
87  protected $type;
88 
95  protected $priority;
96 
102  protected $filters;
103 
110  protected $isFullCoverage;
111 
118  protected $conflictingGroups = [];
119 
126  protected $conflictingFilters = [];
127 
128  private const DEFAULT_PRIORITY = -100;
129 
130  private const RESERVED_NAME_CHAR = '_';
131 
158  public function __construct( array $groupDefinition ) {
159  if ( strpos( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
160  throw new InvalidArgumentException( 'Group names may not contain \'' .
161  self::RESERVED_NAME_CHAR .
162  '\'. Use the naming convention: \'camelCase\''
163  );
164  }
165 
166  $this->name = $groupDefinition['name'];
167 
168  if ( isset( $groupDefinition['title'] ) ) {
169  $this->title = $groupDefinition['title'];
170  }
171 
172  if ( isset( $groupDefinition['whatsThisHeader'] ) ) {
173  $this->whatsThisHeader = $groupDefinition['whatsThisHeader'];
174  $this->whatsThisBody = $groupDefinition['whatsThisBody'];
175  $this->whatsThisUrl = $groupDefinition['whatsThisUrl'];
176  $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText'];
177  }
178 
179  $this->type = $groupDefinition['type'];
180  $this->priority = $groupDefinition['priority'] ?? self::DEFAULT_PRIORITY;
181 
182  $this->isFullCoverage = $groupDefinition['isFullCoverage'];
183 
184  $this->filters = [];
185  $lowestSpecifiedPriority = -1;
186  foreach ( $groupDefinition['filters'] as $filterDefinition ) {
187  if ( isset( $filterDefinition['priority'] ) ) {
188  $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
189  }
190  }
191 
192  // Convenience feature: If you specify a group (and its filters) all in
193  // one place, you don't have to specify priority. You can just put them
194  // in order. However, if you later add one (e.g. an extension adds a filter
195  // to a core-defined group), you need to specify it.
196  $autoFillPriority = $lowestSpecifiedPriority - 1;
197  foreach ( $groupDefinition['filters'] as $filterDefinition ) {
198  if ( !isset( $filterDefinition['priority'] ) ) {
199  $filterDefinition['priority'] = $autoFillPriority;
200  $autoFillPriority--;
201  }
202  $filterDefinition['group'] = $this;
203 
204  $filter = $this->createFilter( $filterDefinition );
205  $this->registerFilter( $filter );
206  }
207  }
208 
215  abstract protected function createFilter( array $filterDefinition );
216 
231  public function conflictsWith( $other, string $globalKey, string $forwardKey, string $backwardKey ) {
233  $other,
234  $globalKey,
235  $forwardKey
236  );
237 
238  $other->setUnidirectionalConflict(
239  $this,
240  $globalKey,
241  $backwardKey
242  );
243  }
244 
256  public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
257  if ( $other instanceof ChangesListFilterGroup ) {
258  $this->conflictingGroups[] = [
259  'group' => $other->getName(),
260  'groupObject' => $other,
261  'globalDescription' => $globalDescription,
262  'contextDescription' => $contextDescription,
263  ];
264  } elseif ( $other instanceof ChangesListFilter ) {
265  $this->conflictingFilters[] = [
266  'group' => $other->getGroup()->getName(),
267  'filter' => $other->getName(),
268  'filterObject' => $other,
269  'globalDescription' => $globalDescription,
270  'contextDescription' => $contextDescription,
271  ];
272  } else {
273  throw new InvalidArgumentException(
274  'You can only pass in a ChangesListFilterGroup or a ChangesListFilter'
275  );
276  }
277  }
278 
282  public function getName() {
283  return $this->name;
284  }
285 
289  public function getTitle() {
290  return $this->title;
291  }
292 
296  public function getType() {
297  return $this->type;
298  }
299 
303  public function getPriority() {
304  return $this->priority;
305  }
306 
311  public function getFilters() {
312  return $this->filters;
313  }
314 
321  public function getFilter( $name ) {
322  return $this->filters[$name] ?? null;
323  }
324 
332  public function getJsData() {
333  $output = [
334  'name' => $this->name,
335  'type' => $this->type,
336  'fullCoverage' => $this->isFullCoverage,
337  'filters' => [],
338  'priority' => $this->priority,
339  'conflicts' => [],
340  'messageKeys' => [ $this->title ]
341  ];
342 
343  if ( isset( $this->whatsThisHeader ) ) {
344  $output['whatsThisHeader'] = $this->whatsThisHeader;
345  $output['whatsThisBody'] = $this->whatsThisBody;
346  $output['whatsThisUrl'] = $this->whatsThisUrl;
347  $output['whatsThisLinkText'] = $this->whatsThisLinkText;
348 
349  array_push(
350  $output['messageKeys'],
351  $output['whatsThisHeader'],
352  $output['whatsThisBody'],
353  $output['whatsThisLinkText']
354  );
355  }
356 
357  usort( $this->filters, static function ( ChangesListFilter $a, ChangesListFilter $b ) {
358  return $b->getPriority() <=> $a->getPriority();
359  } );
360 
361  foreach ( $this->filters as $filter ) {
362  if ( $filter->displaysOnStructuredUi() ) {
363  $filterData = $filter->getJsData();
364  $output['messageKeys'] = array_merge(
365  $output['messageKeys'],
366  $filterData['messageKeys']
367  );
368  unset( $filterData['messageKeys'] );
369  $output['filters'][] = $filterData;
370  }
371  }
372 
373  if ( count( $output['filters'] ) === 0 ) {
374  return null;
375  }
376 
377  $output['title'] = $this->title;
378 
379  $conflicts = array_merge(
380  $this->conflictingGroups,
381  $this->conflictingFilters
382  );
383 
384  foreach ( $conflicts as $conflictInfo ) {
385  unset( $conflictInfo['filterObject'] );
386  unset( $conflictInfo['groupObject'] );
387  $output['conflicts'][] = $conflictInfo;
388  array_push(
389  $output['messageKeys'],
390  $conflictInfo['globalDescription'],
391  $conflictInfo['contextDescription']
392  );
393  }
394 
395  return $output;
396  }
397 
403  public function getConflictingGroups() {
404  return array_column( $this->conflictingGroups, 'groupObject' );
405  }
406 
412  public function getConflictingFilters() {
413  return array_column( $this->conflictingFilters, 'filterObject' );
414  }
415 
422  public function anySelected( FormOptions $opts ) {
423  return (bool)count( array_filter(
424  $this->getFilters(),
425  static function ( ChangesListFilter $filter ) use ( $opts ) {
426  return $filter->isSelected( $opts );
427  }
428  ) );
429  }
430 
447  abstract public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage,
448  &$tables, &$fields, &$conds, &$query_options, &$join_conds,
449  FormOptions $opts, $isStructuredFiltersEnabled );
450 
458  abstract public function addOptions( FormOptions $opts, $allowDefaults,
459  $isStructuredFiltersEnabled );
460 }
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.
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:41
Special page which uses a ChangesList to show query results.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36