MediaWiki  master
ChangesListFilter.php
Go to the documentation of this file.
1 <?php
29 abstract class ChangesListFilter {
35  protected $name;
36 
45  protected $cssClassSuffix;
46 
53 
59  protected $group;
60 
66  protected $label;
67 
73  protected $description;
74 
81  protected $conflictingGroups = [];
82 
89  protected $conflictingFilters = [];
90 
96  protected $subsetFilters = [];
97 
103  protected $priority;
104 
109 
110  private const RESERVED_NAME_CHAR = '_';
111 
143  public function __construct( array $filterDefinition ) {
144  if ( isset( $filterDefinition['group'] ) ) {
145  $this->group = $filterDefinition['group'];
146  } else {
147  throw new MWException( 'You must use \'group\' to specify the ' .
148  'ChangesListFilterGroup this filter belongs to' );
149  }
150 
151  if ( strpos( $filterDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
152  throw new MWException( 'Filter names may not contain \'' .
153  self::RESERVED_NAME_CHAR .
154  '\'. Use the naming convention: \'lowercase\''
155  );
156  }
157 
158  if ( $this->group->getFilter( $filterDefinition['name'] ) ) {
159  throw new MWException( 'Two filters in a group cannot have the ' .
160  "same name: '{$filterDefinition['name']}'" );
161  }
162 
163  $this->name = $filterDefinition['name'];
164 
165  if ( isset( $filterDefinition['cssClassSuffix'] ) ) {
166  $this->cssClassSuffix = $filterDefinition['cssClassSuffix'];
167  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Documented as required
168  $this->isRowApplicableCallable = $filterDefinition['isRowApplicableCallable'];
169  }
170 
171  if ( isset( $filterDefinition['label'] ) ) {
172  $this->label = $filterDefinition['label'];
173  $this->description = $filterDefinition['description'];
174  }
175 
176  $this->priority = $filterDefinition['priority'];
177 
178  $this->group->registerFilter( $this );
179  }
180 
195  public function conflictsWith( $other, $globalKey, $forwardKey, $backwardKey ) {
196  if ( $globalKey === null || $forwardKey === null || $backwardKey === null ) {
197  throw new MWException( 'All messages must be specified' );
198  }
199 
201  $other,
202  $globalKey,
203  $forwardKey
204  );
205 
206  $other->setUnidirectionalConflict(
207  $this,
208  $globalKey,
209  $backwardKey
210  );
211  }
212 
224  public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
225  if ( $other instanceof ChangesListFilterGroup ) {
226  $this->conflictingGroups[] = [
227  'group' => $other->getName(),
228  'groupObject' => $other,
229  'globalDescription' => $globalDescription,
230  'contextDescription' => $contextDescription,
231  ];
232  } elseif ( $other instanceof ChangesListFilter ) {
233  $this->conflictingFilters[] = [
234  'group' => $other->getGroup()->getName(),
235  'filter' => $other->getName(),
236  'filterObject' => $other,
237  'globalDescription' => $globalDescription,
238  'contextDescription' => $contextDescription,
239  ];
240  } else {
241  throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
242  }
243  }
244 
254  public function setAsSupersetOf( ChangesListFilter $other ) {
255  if ( $other->getGroup() !== $this->getGroup() ) {
256  throw new MWException( 'Supersets can only be defined for filters in the same group' );
257  }
258 
259  $this->subsetFilters[] = [
260  // It's always the same group, but this makes the representation
261  // more consistent with conflicts.
262  'group' => $other->getGroup()->getName(),
263  'filter' => $other->getName(),
264  ];
265  }
266 
270  public function getName() {
271  return $this->name;
272  }
273 
277  public function getGroup() {
278  return $this->group;
279  }
280 
284  public function getLabel() {
285  return $this->label;
286  }
287 
291  public function getDescription() {
292  return $this->description;
293  }
294 
300  abstract public function displaysOnUnstructuredUi();
301 
308  public function displaysOnStructuredUi() {
309  return $this->label !== null;
310  }
311 
320  return $this->displaysOnStructuredUi();
321  }
322 
326  public function getPriority() {
327  return $this->priority;
328  }
329 
335  protected function getCssClass() {
336  if ( $this->cssClassSuffix !== null ) {
338  } else {
339  return null;
340  }
341  }
342 
350  public function applyCssClassIfNeeded( IContextSource $ctx, RecentChange $rc, array &$classes ) {
351  if ( $this->isRowApplicableCallable === null ) {
352  return;
353  }
354 
355  if ( call_user_func( $this->isRowApplicableCallable, $ctx, $rc ) ) {
356  $classes[] = $this->getCssClass();
357  }
358  }
359 
367  public function getJsData() {
368  $output = [
369  'name' => $this->getName(),
370  'label' => $this->getLabel(),
371  'description' => $this->getDescription(),
372  'cssClass' => $this->getCssClass(),
373  'priority' => $this->priority,
374  'subset' => $this->subsetFilters,
375  'conflicts' => [],
376  'defaultHighlightColor' => $this->defaultHighlightColor
377  ];
378 
379  $output['messageKeys'] = [
380  $this->getLabel(),
381  $this->getDescription(),
382  ];
383 
384  $conflicts = array_merge(
385  $this->conflictingGroups,
386  $this->conflictingFilters
387  );
388 
389  foreach ( $conflicts as $conflictInfo ) {
390  unset( $conflictInfo['filterObject'] );
391  unset( $conflictInfo['groupObject'] );
392  $output['conflicts'][] = $conflictInfo;
393  array_push(
394  $output['messageKeys'],
395  $conflictInfo['globalDescription'],
396  $conflictInfo['contextDescription']
397  );
398  }
399 
400  return $output;
401  }
402 
409  abstract public function isSelected( FormOptions $opts );
410 
416  public function getConflictingGroups() {
417  return array_map(
418  static function ( $conflictDesc ) {
419  return $conflictDesc[ 'groupObject' ];
420  },
422  );
423  }
424 
430  public function getConflictingFilters() {
431  return array_map(
432  static function ( $conflictDesc ) {
433  return $conflictDesc[ 'filterObject' ];
434  },
436  );
437  }
438 
447  if ( $group->anySelected( $opts ) && $this->isSelected( $opts ) ) {
449  foreach ( $this->getSiblings() as $siblingFilter ) {
450  if ( $siblingFilter->isSelected( $opts ) && !$siblingFilter->hasConflictWithGroup( $group ) ) {
451  return false;
452  }
453  }
454  return true;
455  }
456  return false;
457  }
458 
460  return in_array( $group, $this->getConflictingGroups() );
461  }
462 
470  public function activelyInConflictWithFilter( ChangesListFilter $filter, FormOptions $opts ) {
471  if ( $this->isSelected( $opts ) && $filter->isSelected( $opts ) ) {
473  foreach ( $this->getSiblings() as $siblingFilter ) {
474  if (
475  $siblingFilter->isSelected( $opts ) &&
476  !$siblingFilter->hasConflictWithFilter( $filter )
477  ) {
478  return false;
479  }
480  }
481  return true;
482  }
483  return false;
484  }
485 
486  private function hasConflictWithFilter( ChangesListFilter $filter ) {
487  return in_array( $filter, $this->getConflictingFilters() );
488  }
489 
495  protected function getSiblings() {
496  return array_filter(
497  $this->getGroup()->getFilters(),
498  function ( $filter ) {
499  return $filter !== $this;
500  }
501  );
502  }
503 
508  $this->defaultHighlightColor = $defaultHighlightColor;
509  }
510 }
Represents a filter group (used on ChangesListSpecialPage and descendants)
anySelected(FormOptions $opts)
Check if any filter in this group is selected.
Represents a filter (used on ChangesListSpecialPage and descendants)
displaysOnUnstructuredUi()
Checks whether the filter should display on the unstructured UI.
string null $cssClassSuffix
CSS class suffix used for attribution, e.g.
getConflictingFilters()
Get filters conflicting with this filter.
ChangesListFilterGroup $group
Group.
getJsData()
Gets the JS data required by the front-end of the structured UI.
activelyInConflictWithGroup(ChangesListFilterGroup $group, FormOptions $opts)
Check if the conflict with a group is currently "active".
array $conflictingGroups
Array of associative arrays with conflict information.
int $priority
Priority integer.
array $conflictingFilters
Array of associative arrays with conflict information.
displaysOnStructuredUi()
Checks whether the filter should display on the structured UI This refers to the exact filter.
string $label
i18n key of label for structured UI
setUnidirectionalConflict( $other, $globalDescription, $contextDescription)
Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
string $name
Filter name.
isFeatureAvailableOnStructuredUi()
Checks whether an equivalent feature for this filter is available on the structured UI.
applyCssClassIfNeeded(IContextSource $ctx, RecentChange $rc, array &$classes)
Add CSS class if needed.
setAsSupersetOf(ChangesListFilter $other)
Marks that the current instance is (also) a superset of the filter passed in.
__construct(array $filterDefinition)
Creates a new filter with the specified configuration, and registers it to the specified group.
hasConflictWithGroup(ChangesListFilterGroup $group)
array $subsetFilters
Array of associative arrays with subset information.
isSelected(FormOptions $opts)
Checks whether this filter is selected in the provided options.
setDefaultHighlightColor( $defaultHighlightColor)
string $description
i18n key of description for structured UI
getConflictingGroups()
Get groups conflicting with this filter.
hasConflictWithFilter(ChangesListFilter $filter)
getCssClass()
Gets the CSS class.
getSiblings()
Get filters in the same group.
activelyInConflictWithFilter(ChangesListFilter $filter, FormOptions $opts)
Check if the conflict with a filter is currently "active".
callable $isRowApplicableCallable
Callable that returns true if and only if a row is attributed to this filter.
conflictsWith( $other, $globalKey, $forwardKey, $backwardKey)
Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
const CSS_CLASS_PREFIX
Definition: ChangesList.php:40
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
MediaWiki exception.
Definition: MWException.php:29
Utility class for creating new RC entries.
Interface for objects which can provide a MediaWiki context on request.