MediaWiki REL1_37
IndexPager.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
30
74abstract class IndexPager extends ContextSource implements Pager {
75 use ProtectedHookAccessorTrait;
76
78 public const DIR_ASCENDING = false;
80 public const DIR_DESCENDING = true;
81
83 public const QUERY_ASCENDING = true;
85 public const QUERY_DESCENDING = false;
86
88 public $mRequest;
90 public $mLimitsShown = [ 20, 50, 100, 250, 500 ];
92 public $mDefaultLimit = 50;
94 public $mOffset;
96 public $mLimit;
98 public $mQueryDone = false;
100 public $mDb;
103
110 protected $mIndexField;
120 protected $mOrderType;
136
138 public $mIsFirst;
140 public $mIsLast;
141
143 protected $mLastShown;
145 protected $mFirstShown;
149 protected $mDefaultQuery;
152
157 protected $mIncludeOffset = false;
158
164 public $mResult;
165
168
176 if ( $context ) {
177 $this->setContext( $context );
178 }
179
180 $this->mRequest = $this->getRequest();
181
182 # NB: the offset is quoted, not validated. It is treated as an
183 # arbitrary string to support the widest variety of index types. Be
184 # careful outputting it into HTML!
185 $this->mOffset = $this->mRequest->getText( 'offset' );
186
187 # Use consistent behavior for the limit options
188 $this->mDefaultLimit = MediaWikiServices::getInstance()
189 ->getUserOptionsLookup()
190 ->getIntOption( $this->getUser(), 'rclimit' );
191 if ( !$this->mLimit ) {
192 // Don't override if a subclass calls $this->setLimit() in its constructor.
193 list( $this->mLimit, /* $offset */ ) = $this->mRequest
194 ->getLimitOffsetForUser( $this->getUser() );
195 }
196
197 $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
198 # Let the subclass set the DB here; otherwise use a replica DB for the current wiki
199 $this->mDb = $this->mDb ?: wfGetDB( DB_REPLICA );
200
201 $index = $this->getIndexField(); // column to sort on
202 $extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning
203 $order = $this->mRequest->getVal( 'order' );
204
205 if ( is_array( $index ) && isset( $index[$order] ) ) {
206 $this->mOrderType = $order;
207 $this->mIndexField = $index[$order];
208 $this->mExtraSortFields = isset( $extraSort[$order] )
209 ? (array)$extraSort[$order]
210 : [];
211 } elseif ( is_array( $index ) ) {
212 # First element is the default
213 $this->mIndexField = reset( $index );
214 $this->mOrderType = key( $index );
215 $this->mExtraSortFields = isset( $extraSort[$this->mOrderType] )
216 ? (array)$extraSort[$this->mOrderType]
217 : [];
218 } else {
219 # $index is not an array
220 $this->mOrderType = null;
221 $this->mIndexField = $index;
222 $isSortAssociative = array_values( $extraSort ) !== $extraSort;
223 if ( $isSortAssociative ) {
224 $this->mExtraSortFields = isset( $extraSort[$index] )
225 ? (array)$extraSort[$index]
226 : [];
227 } else {
228 $this->mExtraSortFields = (array)$extraSort;
229 }
230 }
231
232 if ( !isset( $this->mDefaultDirection ) ) {
233 $dir = $this->getDefaultDirections();
234 $this->mDefaultDirection = is_array( $dir )
235 ? $dir[$this->mOrderType]
236 : $dir;
237 }
238 $this->linkRenderer = $linkRenderer;
239 }
240
246 public function getDatabase() {
247 return $this->mDb;
248 }
249
257 public function doQuery() {
258 $defaultOrder = ( $this->mDefaultDirection === self::DIR_ASCENDING )
259 ? self::QUERY_ASCENDING
261 $order = $this->mIsBackwards ? self::oppositeOrder( $defaultOrder ) : $defaultOrder;
262
263 # Plus an extra row so that we can tell the "next" link should be shown
264 $queryLimit = $this->mLimit + 1;
265
266 if ( $this->mOffset == '' ) {
267 $isFirst = true;
268 } else {
269 // If there's an offset, we may or may not be at the first entry.
270 // The only way to tell is to run the query in the opposite
271 // direction see if we get a row.
272 $oldIncludeOffset = $this->mIncludeOffset;
273 $this->mIncludeOffset = !$this->mIncludeOffset;
274 $oppositeOrder = self::oppositeOrder( $order );
275 $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, $oppositeOrder )->numRows();
276 $this->mIncludeOffset = $oldIncludeOffset;
277 }
278
279 $this->mResult = $this->reallyDoQuery(
280 $this->mOffset,
281 $queryLimit,
282 $order
283 );
284
285 $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult );
286 $this->mQueryDone = true;
287
288 $this->preprocessResults( $this->mResult );
289 $this->mResult->rewind(); // Paranoia
290 }
291
296 final protected static function oppositeOrder( $order ) {
297 return ( $order === self::QUERY_ASCENDING )
300 }
301
305 public function getResult() {
306 return $this->mResult;
307 }
308
314 public function setOffset( $offset ) {
315 $this->mOffset = $offset;
316 }
317
327 public function setLimit( $limit ) {
328 $limit = (int)$limit;
329 // WebRequest::getLimitOffsetForUser() puts a cap of 5000, so do same here.
330 if ( $limit > 5000 ) {
331 $limit = 5000;
332 }
333 if ( $limit > 0 ) {
334 $this->mLimit = $limit;
335 }
336 }
337
343 public function getLimit() {
344 return $this->mLimit;
345 }
346
354 public function setIncludeOffset( $include ) {
355 $this->mIncludeOffset = $include;
356 }
357
369 protected function extractResultInfo( $isFirst, $limit, IResultWrapper $res ) {
370 $numRows = $res->numRows();
371
372 $firstIndex = [];
373 $lastIndex = [];
374 $this->mPastTheEndIndex = [];
375 $this->mPastTheEndRow = null;
376
377 if ( $numRows ) {
378 $indexColumns = array_map( static function ( $v ) {
379 // Remove any table prefix from index field
380 $parts = explode( '.', $v );
381 return end( $parts );
382 }, (array)$this->mIndexField );
383
384 $row = $res->fetchRow();
385 foreach ( $indexColumns as $indexColumn ) {
386 $firstIndex[] = $row[$indexColumn];
387 }
388
389 # Discard the extra result row if there is one
390 if ( $numRows > $this->mLimit && $numRows > 1 ) {
391 $res->seek( $numRows - 1 );
392 $this->mPastTheEndRow = $res->fetchObject();
393 foreach ( $indexColumns as $indexColumn ) {
394 $this->mPastTheEndIndex[] = $this->mPastTheEndRow->$indexColumn;
395 }
396 $res->seek( $numRows - 2 );
397 $row = $res->fetchRow();
398 foreach ( $indexColumns as $indexColumn ) {
399 $lastIndex[] = $row[$indexColumn];
400 }
401 } else {
402 $this->mPastTheEndRow = null;
403 $res->seek( $numRows - 1 );
404 $row = $res->fetchRow();
405 foreach ( $indexColumns as $indexColumn ) {
406 $lastIndex[] = $row[$indexColumn];
407 }
408 }
409 }
410
411 if ( $this->mIsBackwards ) {
412 $this->mIsFirst = ( $numRows < $limit );
413 $this->mIsLast = $isFirst;
414 $this->mLastShown = $firstIndex;
415 $this->mFirstShown = $lastIndex;
416 } else {
417 $this->mIsFirst = $isFirst;
418 $this->mIsLast = ( $numRows < $limit );
419 $this->mLastShown = $lastIndex;
420 $this->mFirstShown = $firstIndex;
421 }
422 }
423
431 protected function getSqlComment() {
432 return static::class;
433 }
434
447 public function reallyDoQuery( $offset, $limit, $order ) {
448 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
449 $this->buildQueryInfo( $offset, $limit, $order );
450
451 return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
452 }
453
466 protected function buildQueryInfo( $offset, $limit, $order ) {
467 $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
468 $info = $this->getQueryInfo();
469 $tables = $info['tables'];
470 $fields = $info['fields'];
471 $conds = $info['conds'] ?? [];
472 $options = $info['options'] ?? [];
473 $join_conds = $info['join_conds'] ?? [];
474 $indexColumns = (array)$this->mIndexField;
475 $sortColumns = array_merge( $indexColumns, $this->mExtraSortFields );
476
477 if ( $order === self::QUERY_ASCENDING ) {
478 $options['ORDER BY'] = $sortColumns;
479 $operator = $this->mIncludeOffset ? '>=' : '>';
480 } else {
481 $orderBy = [];
482 foreach ( $sortColumns as $col ) {
483 $orderBy[] = $col . ' DESC';
484 }
485 $options['ORDER BY'] = $orderBy;
486 $operator = $this->mIncludeOffset ? '<=' : '<';
487 }
488 if ( $offset != '' ) {
489 $offsets = explode( '|', $offset, /* Limit to max of indices */ count( $indexColumns ) );
490
491 $conds[] = $this->buildOffsetConds(
492 $offsets,
493 $indexColumns,
494 $operator
495 );
496 }
497 $options['LIMIT'] = intval( $limit );
498 return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
499 }
500
537 private function buildOffsetConds( $offsets, $columns, $operator ) {
538 $innerConds = [];
539 // $offsets and $columns are the same length
540 for ( $i = 1; $i <= count( $offsets ); $i++ ) {
541 $innerConds[] = $this->buildOffsetInnerConds(
542 array_slice( $offsets, 0, $i ),
543 array_slice( $columns, 0, $i ),
544 $operator
545 );
546 }
547 return $this->mDb->makeList( $innerConds, IDatabase::LIST_OR );
548 }
549
559 private function buildOffsetInnerConds( $offsets, $columns, $operator ) {
560 $conds = [];
561 while ( count( $offsets ) > 1 ) {
562 $conds[] = $columns[0] . '=' . $this->mDb->addQuotes( $offsets[0] );
563 array_shift( $columns );
564 array_shift( $offsets );
565 }
566 $conds[] = $columns[0] . $operator . $this->mDb->addQuotes( $offsets[0] );
567 return $this->mDb->makeList( $conds, IDatabase::LIST_AND );
568 }
569
577 protected function preprocessResults( $result ) {
578 }
579
588 public function getBody() {
589 if ( !$this->mQueryDone ) {
590 $this->doQuery();
591 }
592
593 if ( $this->mResult->numRows() ) {
594 # Do any special query batches before display
595 $this->doBatchLookups();
596 }
597
598 # Don't use any extra rows returned by the query
599 $numRows = min( $this->mResult->numRows(), $this->mLimit );
600
601 $s = $this->getStartBody();
602 if ( $numRows ) {
603 if ( $this->mIsBackwards ) {
604 for ( $i = $numRows - 1; $i >= 0; $i-- ) {
605 $this->mResult->seek( $i );
606 $row = $this->mResult->fetchObject();
607 $s .= $this->formatRow( $row );
608 }
609 } else {
610 $this->mResult->seek( 0 );
611 for ( $i = 0; $i < $numRows; $i++ ) {
612 $row = $this->mResult->fetchObject();
613 $s .= $this->formatRow( $row );
614 }
615 }
616 } else {
617 $s .= $this->getEmptyBody();
618 }
619 $s .= $this->getEndBody();
620 return $s;
621 }
622
634 protected function makeLink( $text, array $query = null, $type = null ) {
635 if ( $query === null ) {
636 return $text;
637 }
638
639 $attrs = [];
640 if ( in_array( $type, [ 'prev', 'next' ] ) ) {
641 $attrs['rel'] = $type;
642 }
643
644 if ( in_array( $type, [ 'asc', 'desc' ] ) ) {
645 $attrs['title'] = $this->msg( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
646 }
647
648 if ( $type ) {
649 $attrs['class'] = "mw-{$type}link";
650 }
651
652 return $this->getLinkRenderer()->makeKnownLink(
653 $this->getTitle(),
654 new HtmlArmor( $text ),
655 $attrs,
656 $query + $this->getDefaultQuery()
657 );
658 }
659
669 protected function doBatchLookups() {
670 }
671
678 protected function getStartBody() {
679 return '';
680 }
681
689 protected function getEndBody() {
690 return '';
691 }
692
701 protected function getEmptyBody() {
702 return '';
703 }
704
714 public function getDefaultQuery() {
715 if ( !isset( $this->mDefaultQuery ) ) {
716 $this->mDefaultQuery = $this->getRequest()->getQueryValues();
717 unset( $this->mDefaultQuery['title'] );
718 unset( $this->mDefaultQuery['dir'] );
719 unset( $this->mDefaultQuery['offset'] );
720 unset( $this->mDefaultQuery['limit'] );
721 unset( $this->mDefaultQuery['order'] );
722 unset( $this->mDefaultQuery['month'] );
723 unset( $this->mDefaultQuery['year'] );
724 }
726 }
727
733 public function getNumRows() {
734 if ( !$this->mQueryDone ) {
735 $this->doQuery();
736 }
737 return $this->mResult->numRows();
738 }
739
747 public function getPagingQueries() {
748 if ( !$this->mQueryDone ) {
749 $this->doQuery();
750 }
751
752 # Don't announce the limit everywhere if it's the default
753 $urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit;
754
755 if ( $this->mIsFirst ) {
756 $prev = false;
757 $first = false;
758 } else {
759 $prev = [
760 'dir' => 'prev',
761 'offset' => implode( '|', (array)$this->mFirstShown ),
762 'limit' => $urlLimit
763 ];
764 $first = [ 'limit' => $urlLimit ];
765 }
766 if ( $this->mIsLast ) {
767 $next = false;
768 $last = false;
769 } else {
770 $next = [ 'offset' => implode( '|', (array)$this->mLastShown ), 'limit' => $urlLimit ];
771 $last = [ 'dir' => 'prev', 'limit' => $urlLimit ];
772 }
773
774 return [
775 'prev' => $prev,
776 'next' => $next,
777 'first' => $first,
778 'last' => $last
779 ];
780 }
781
788 protected function isNavigationBarShown() {
789 if ( !$this->mQueryDone ) {
790 $this->doQuery();
791 }
792 // Hide navigation by default if there is nothing to page
793 return !( $this->mIsFirst && $this->mIsLast );
794 }
795
806 protected function getPagingLinks( $linkTexts, $disabledTexts = [] ) {
807 $queries = $this->getPagingQueries();
808 $links = [];
809
810 foreach ( $queries as $type => $query ) {
811 if ( $query !== false ) {
812 $links[$type] = $this->makeLink(
813 $linkTexts[$type],
814 $query,
815 $type
816 );
817 } elseif ( isset( $disabledTexts[$type] ) ) {
818 $links[$type] = $disabledTexts[$type];
819 } else {
820 $links[$type] = $linkTexts[$type];
821 }
822 }
823
824 return $links;
825 }
826
827 protected function getLimitLinks() {
828 $links = [];
829 if ( $this->mIsBackwards ) {
830 $offset = implode( '|', (array)$this->mPastTheEndIndex );
831 } else {
832 $offset = $this->mOffset;
833 }
834 foreach ( $this->mLimitsShown as $limit ) {
835 $links[] = $this->makeLink(
836 $this->getLanguage()->formatNum( $limit ),
837 [ 'offset' => $offset, 'limit' => $limit ],
838 'num'
839 );
840 }
841 return $links;
842 }
843
851 abstract public function formatRow( $row );
852
864 abstract public function getQueryInfo();
865
902 abstract public function getIndexField();
903
927 protected function getExtraSortFields() {
928 return [];
929 }
930
952 protected function getDefaultDirections() {
953 return self::DIR_ASCENDING;
954 }
955
966 protected function buildPrevNextNavigation(
968 $offset,
969 $limit,
970 array $query = [],
971 $atend = false
972 ) {
973 $prevNext = new PrevNextNavigationRenderer( $this );
974
975 return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query, $atend );
976 }
977
982 protected function getLinkRenderer() {
983 if ( $this->linkRenderer === null ) {
984 $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
985 }
986 return $this->linkRenderer;
987 }
988}
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
setContext(IContextSource $context)
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
IndexPager is an efficient pager which uses a (roughly unique) index in the data set to implement pag...
getQueryInfo()
Provides all parameters needed for the main paged query.
getEndBody()
Hook into getBody() for the end of the list.
const QUERY_DESCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
string[] $mExtraSortFields
An array of secondary columns to order by.
array $mDefaultQuery
int $mLimit
The maximum number of entries to show.
bool $mDefaultDirection
$mDefaultDirection gives the direction to use when sorting results: DIR_ASCENDING or DIR_DESCENDING.
WebRequest $mRequest
getDefaultDirections()
Return the default sorting direction: DIR_ASCENDING or DIR_DESCENDING.
setOffset( $offset)
Set the offset from an other source than the request.
makeLink( $text, array $query=null, $type=null)
Make a self-link.
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
IResultWrapper $mResult
Result object for the query.
getEmptyBody()
Hook into getBody(), for the bit between the start and the end when there are no rows.
setIncludeOffset( $include)
Set whether a row matching exactly the offset should be also included in the result or not.
IDatabase $mDb
bool $mQueryDone
Whether the listing query completed.
extractResultInfo( $isFirst, $limit, IResultWrapper $res)
Extract some useful data from the result object for use by the navigation bar, put it into $this.
getLimit()
Get the current limit.
getSqlComment()
Get some text to go in brackets in the "function name" part of the SQL comment.
string $mNavigationBar
bool $mIncludeOffset
Whether to include the offset in the query.
static oppositeOrder( $order)
getPagingLinks( $linkTexts, $disabledTexts=[])
Get paging links.
mixed $mOffset
The starting point to enumerate entries.
int[] $mLimitsShown
List of default entry limit options to be presented to clients.
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
string null $mOrderType
For pages that support multiple types of ordering, which one to use.
buildOffsetConds( $offsets, $columns, $operator)
Build the conditions for the offset, given that we may be paginating on a single column or multiple c...
getPagingQueries()
Get a URL query array for the prev, next, first and last links.
string string[] $mIndexField
The index to actually be used for ordering.
getStartBody()
Hook into getBody(), allows text to be inserted at the start.
__construct(IContextSource $context=null, LinkRenderer $linkRenderer=null)
bool $mIsFirst
True if the current result set is the first one.
getDatabase()
Get the Database object in use.
array $mLastShown
formatRow( $row)
Returns an HTML string representing the result row $row.
array $mFirstShown
getBody()
Get the formatted result list.
LinkRenderer $linkRenderer
buildPrevNextNavigation(Title $title, $offset, $limit, array $query=[], $atend=false)
Generate (prev x| next x) (20|50|100...) type links for paging.
int $mDefaultLimit
The default entry limit choosen for clients.
doQuery()
Do the query, using information from the object context.
getNumRows()
Get the number of rows in the result set.
isNavigationBarShown()
Returns whether to show the "navigation bar".
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
getIndexField()
Returns the name of the index field.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
stdClass bool null $mPastTheEndRow
Extra row fetched at the end to see if the end was reached.
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
bool $mIsBackwards
array $mPastTheEndIndex
preprocessResults( $result)
Pre-process results; useful for performing batch existence checks, etc.
getExtraSortFields()
Returns the names of secondary columns to order by in addition to the column in getIndexField().
buildOffsetInnerConds( $offsets, $columns, $operator)
Build an inner part of an offset condition, consisting of inequalities joined by AND,...
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
setLimit( $limit)
Set the limit from an other source than the request.
reallyDoQuery( $offset, $limit, $order)
Do a query with specified parameters, rather than using the object context.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Helper class for generating prev/next links for paging.
Represents a title within MediaWiki.
Definition Title.php:48
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Interface for objects which can provide a MediaWiki context on request.
Basic pager interface.
Definition Pager.php:34
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Result wrapper for grabbing data queried from an IDatabase object.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
const DB_REPLICA
Definition defines.php:25