MediaWiki REL1_34
IndexPager.php
Go to the documentation of this file.
1<?php
29
72abstract class IndexPager extends ContextSource implements Pager {
74 const DIR_ASCENDING = false;
76 const DIR_DESCENDING = true;
77
79 const QUERY_ASCENDING = true;
81 const QUERY_DESCENDING = false;
82
84 public $mRequest;
86 public $mLimitsShown = [ 20, 50, 100, 250, 500 ];
88 public $mDefaultLimit = 50;
90 public $mOffset;
92 public $mLimit;
94 public $mQueryDone = false;
96 public $mDb;
99
105 protected $mIndexField;
115 protected $mOrderType;
131
133 public $mIsFirst;
135 public $mIsLast;
136
138 protected $mLastShown;
140 protected $mFirstShown;
144 protected $mDefaultQuery;
147
152 protected $mIncludeOffset = false;
153
159 public $mResult;
160
163
165 if ( $context ) {
166 $this->setContext( $context );
167 }
168
169 $this->mRequest = $this->getRequest();
170
171 # NB: the offset is quoted, not validated. It is treated as an
172 # arbitrary string to support the widest variety of index types. Be
173 # careful outputting it into HTML!
174 $this->mOffset = $this->mRequest->getText( 'offset' );
175
176 # Use consistent behavior for the limit options
177 $this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' );
178 if ( !$this->mLimit ) {
179 // Don't override if a subclass calls $this->setLimit() in its constructor.
180 list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
181 }
182
183 $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
184 # Let the subclass set the DB here; otherwise use a replica DB for the current wiki
185 $this->mDb = $this->mDb ?: wfGetDB( DB_REPLICA );
186
187 $index = $this->getIndexField(); // column to sort on
188 $extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning
189 $order = $this->mRequest->getVal( 'order' );
190 if ( is_array( $index ) && isset( $index[$order] ) ) {
191 $this->mOrderType = $order;
192 $this->mIndexField = $index[$order];
193 $this->mExtraSortFields = isset( $extraSort[$order] )
194 ? (array)$extraSort[$order]
195 : [];
196 } elseif ( is_array( $index ) ) {
197 # First element is the default
198 $this->mIndexField = reset( $index );
199 $this->mOrderType = key( $index );
200 $this->mExtraSortFields = isset( $extraSort[$this->mOrderType] )
201 ? (array)$extraSort[$this->mOrderType]
202 : [];
203 } else {
204 # $index is not an array
205 $this->mOrderType = null;
206 $this->mIndexField = $index;
207 $this->mExtraSortFields = (array)$extraSort;
208 }
209
210 if ( !isset( $this->mDefaultDirection ) ) {
211 $dir = $this->getDefaultDirections();
212 $this->mDefaultDirection = is_array( $dir )
213 ? $dir[$this->mOrderType]
214 : $dir;
215 }
216 $this->linkRenderer = $linkRenderer;
217 }
218
224 public function getDatabase() {
225 return $this->mDb;
226 }
227
233 public function doQuery() {
234 # Use the child class name for profiling
235 $fname = __METHOD__ . ' (' . static::class . ')';
237 $section = Profiler::instance()->scopedProfileIn( $fname );
238
239 $defaultOrder = ( $this->mDefaultDirection === self::DIR_ASCENDING )
240 ? self::QUERY_ASCENDING
242 $order = $this->mIsBackwards ? self::oppositeOrder( $defaultOrder ) : $defaultOrder;
243
244 # Plus an extra row so that we can tell the "next" link should be shown
245 $queryLimit = $this->mLimit + 1;
246
247 if ( $this->mOffset == '' ) {
248 $isFirst = true;
249 } else {
250 // If there's an offset, we may or may not be at the first entry.
251 // The only way to tell is to run the query in the opposite
252 // direction see if we get a row.
253 $oldIncludeOffset = $this->mIncludeOffset;
254 $this->mIncludeOffset = !$this->mIncludeOffset;
255 $oppositeOrder = self::oppositeOrder( $order );
256 $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, $oppositeOrder )->numRows();
257 $this->mIncludeOffset = $oldIncludeOffset;
258 }
259
260 $this->mResult = $this->reallyDoQuery(
261 $this->mOffset,
262 $queryLimit,
263 $order
264 );
265
266 $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult );
267 $this->mQueryDone = true;
268
269 $this->preprocessResults( $this->mResult );
270 $this->mResult->rewind(); // Paranoia
271 }
272
277 final protected static function oppositeOrder( $order ) {
278 return ( $order === self::QUERY_ASCENDING )
281 }
282
286 function getResult() {
287 return $this->mResult;
288 }
289
295 function setOffset( $offset ) {
296 $this->mOffset = $offset;
297 }
298
306 function setLimit( $limit ) {
307 $limit = (int)$limit;
308 // WebRequest::getLimitOffset() puts a cap of 5000, so do same here.
309 if ( $limit > 5000 ) {
310 $limit = 5000;
311 }
312 if ( $limit > 0 ) {
313 $this->mLimit = $limit;
314 }
315 }
316
322 function getLimit() {
323 return $this->mLimit;
324 }
325
333 public function setIncludeOffset( $include ) {
334 $this->mIncludeOffset = $include;
335 }
336
346 function extractResultInfo( $isFirst, $limit, IResultWrapper $res ) {
347 $numRows = $res->numRows();
348 if ( $numRows ) {
349 # Remove any table prefix from index field
350 $parts = explode( '.', $this->mIndexField );
351 $indexColumn = end( $parts );
352
353 $row = $res->fetchRow();
354 $firstIndex = $row[$indexColumn];
355
356 # Discard the extra result row if there is one
357 if ( $numRows > $this->mLimit && $numRows > 1 ) {
358 $res->seek( $numRows - 1 );
359 $this->mPastTheEndRow = $res->fetchObject();
360 $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn;
361 $res->seek( $numRows - 2 );
362 $row = $res->fetchRow();
363 $lastIndex = $row[$indexColumn];
364 } else {
365 $this->mPastTheEndRow = null;
366 # Setting indexes to an empty string means that they will be
367 # omitted if they would otherwise appear in URLs. It just so
368 # happens that this is the right thing to do in the standard
369 # UI, in all the relevant cases.
370 $this->mPastTheEndIndex = '';
371 $res->seek( $numRows - 1 );
372 $row = $res->fetchRow();
373 $lastIndex = $row[$indexColumn];
374 }
375 } else {
376 $firstIndex = '';
377 $lastIndex = '';
378 $this->mPastTheEndRow = null;
379 $this->mPastTheEndIndex = '';
380 }
381
382 if ( $this->mIsBackwards ) {
383 $this->mIsFirst = ( $numRows < $limit );
384 $this->mIsLast = $isFirst;
385 $this->mLastShown = $firstIndex;
386 $this->mFirstShown = $lastIndex;
387 } else {
388 $this->mIsFirst = $isFirst;
389 $this->mIsLast = ( $numRows < $limit );
390 $this->mLastShown = $lastIndex;
391 $this->mFirstShown = $firstIndex;
392 }
393 }
394
400 function getSqlComment() {
401 return static::class;
402 }
403
414 public function reallyDoQuery( $offset, $limit, $order ) {
415 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
416 $this->buildQueryInfo( $offset, $limit, $order );
417
418 return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
419 }
420
431 protected function buildQueryInfo( $offset, $limit, $order ) {
432 $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
433 $info = $this->getQueryInfo();
434 $tables = $info['tables'];
435 $fields = $info['fields'];
436 $conds = $info['conds'] ?? [];
437 $options = $info['options'] ?? [];
438 $join_conds = $info['join_conds'] ?? [];
439 $sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields );
440 if ( $order === self::QUERY_ASCENDING ) {
441 $options['ORDER BY'] = $sortColumns;
442 $operator = $this->mIncludeOffset ? '>=' : '>';
443 } else {
444 $orderBy = [];
445 foreach ( $sortColumns as $col ) {
446 $orderBy[] = $col . ' DESC';
447 }
448 $options['ORDER BY'] = $orderBy;
449 $operator = $this->mIncludeOffset ? '<=' : '<';
450 }
451 if ( $offset != '' ) {
452 $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
453 }
454 $options['LIMIT'] = intval( $limit );
455 return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
456 }
457
463 protected function preprocessResults( $result ) {
464 }
465
472 public function getBody() {
473 if ( !$this->mQueryDone ) {
474 $this->doQuery();
475 }
476
477 if ( $this->mResult->numRows() ) {
478 # Do any special query batches before display
479 $this->doBatchLookups();
480 }
481
482 # Don't use any extra rows returned by the query
483 $numRows = min( $this->mResult->numRows(), $this->mLimit );
484
485 $s = $this->getStartBody();
486 if ( $numRows ) {
487 if ( $this->mIsBackwards ) {
488 for ( $i = $numRows - 1; $i >= 0; $i-- ) {
489 $this->mResult->seek( $i );
490 $row = $this->mResult->fetchObject();
491 $s .= $this->formatRow( $row );
492 }
493 } else {
494 $this->mResult->seek( 0 );
495 for ( $i = 0; $i < $numRows; $i++ ) {
496 $row = $this->mResult->fetchObject();
497 $s .= $this->formatRow( $row );
498 }
499 }
500 } else {
501 $s .= $this->getEmptyBody();
502 }
503 $s .= $this->getEndBody();
504 return $s;
505 }
506
516 function makeLink( $text, array $query = null, $type = null ) {
517 if ( $query === null ) {
518 return $text;
519 }
520
521 $attrs = [];
522 if ( in_array( $type, [ 'prev', 'next' ] ) ) {
523 $attrs['rel'] = $type;
524 }
525
526 if ( in_array( $type, [ 'asc', 'desc' ] ) ) {
527 $attrs['title'] = $this->msg( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
528 }
529
530 if ( $type ) {
531 $attrs['class'] = "mw-{$type}link";
532 }
533
534 return $this->getLinkRenderer()->makeKnownLink(
535 $this->getTitle(),
536 new HtmlArmor( $text ),
537 $attrs,
538 $query + $this->getDefaultQuery()
539 );
540 }
541
549 protected function doBatchLookups() {
550 }
551
558 protected function getStartBody() {
559 return '';
560 }
561
567 protected function getEndBody() {
568 return '';
569 }
570
577 protected function getEmptyBody() {
578 return '';
579 }
580
588 function getDefaultQuery() {
589 if ( !isset( $this->mDefaultQuery ) ) {
590 $this->mDefaultQuery = $this->getRequest()->getQueryValues();
591 unset( $this->mDefaultQuery['title'] );
592 unset( $this->mDefaultQuery['dir'] );
593 unset( $this->mDefaultQuery['offset'] );
594 unset( $this->mDefaultQuery['limit'] );
595 unset( $this->mDefaultQuery['order'] );
596 unset( $this->mDefaultQuery['month'] );
597 unset( $this->mDefaultQuery['year'] );
598 }
600 }
601
607 function getNumRows() {
608 if ( !$this->mQueryDone ) {
609 $this->doQuery();
610 }
611 return $this->mResult->numRows();
612 }
613
619 function getPagingQueries() {
620 if ( !$this->mQueryDone ) {
621 $this->doQuery();
622 }
623
624 # Don't announce the limit everywhere if it's the default
625 $urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit;
626
627 if ( $this->mIsFirst ) {
628 $prev = false;
629 $first = false;
630 } else {
631 $prev = [
632 'dir' => 'prev',
633 'offset' => $this->mFirstShown,
634 'limit' => $urlLimit
635 ];
636 $first = [ 'limit' => $urlLimit ];
637 }
638 if ( $this->mIsLast ) {
639 $next = false;
640 $last = false;
641 } else {
642 $next = [ 'offset' => $this->mLastShown, 'limit' => $urlLimit ];
643 $last = [ 'dir' => 'prev', 'limit' => $urlLimit ];
644 }
645 return [
646 'prev' => $prev,
647 'next' => $next,
648 'first' => $first,
649 'last' => $last
650 ];
651 }
652
659 if ( !$this->mQueryDone ) {
660 $this->doQuery();
661 }
662 // Hide navigation by default if there is nothing to page
663 return !( $this->mIsFirst && $this->mIsLast );
664 }
665
676 function getPagingLinks( $linkTexts, $disabledTexts = [] ) {
677 $queries = $this->getPagingQueries();
678 $links = [];
679
680 foreach ( $queries as $type => $query ) {
681 if ( $query !== false ) {
682 $links[$type] = $this->makeLink(
683 $linkTexts[$type],
685 $type
686 );
687 } elseif ( isset( $disabledTexts[$type] ) ) {
688 $links[$type] = $disabledTexts[$type];
689 } else {
690 $links[$type] = $linkTexts[$type];
691 }
692 }
693
694 return $links;
695 }
696
697 function getLimitLinks() {
698 $links = [];
699 if ( $this->mIsBackwards ) {
700 $offset = $this->mPastTheEndIndex;
701 } else {
702 $offset = $this->mOffset;
703 }
704 foreach ( $this->mLimitsShown as $limit ) {
705 $links[] = $this->makeLink(
706 $this->getLanguage()->formatNum( $limit ),
707 [ 'offset' => $offset, 'limit' => $limit ],
708 'num'
709 );
710 }
711 return $links;
712 }
713
722 abstract function formatRow( $row );
723
736 abstract function getQueryInfo();
737
750 abstract function getIndexField();
751
768 protected function getExtraSortFields() {
769 return [];
770 }
771
791 protected function getDefaultDirections() {
792 return self::DIR_ASCENDING;
793 }
794
805 protected function buildPrevNextNavigation( Title $title, $offset, $limit,
806 array $query = [], $atend = false
807 ) {
808 $prevNext = new PrevNextNavigationRenderer( $this );
809
810 return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query, $atend );
811 }
812
813 protected function getLinkRenderer() {
814 if ( $this->linkRenderer === null ) {
815 $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
816 }
817 return $this->linkRenderer;
818 }
819}
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:28
IndexPager is an efficient pager which uses a (roughly unique) index in the data set to implement pag...
getQueryInfo()
This function should be overridden to provide 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.
getPagingQueries()
Get a URL query array for the prev, next, first and last links.
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.
formatRow( $row)
Abstract formatting function.
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".
mixed $mLastShown
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
getIndexField()
This function should be overridden to return the name of the index fi- eld.
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
preprocessResults( $result)
Pre-process results; useful for performing batch existence checks, etc.
getExtraSortFields()
This function should be overridden to return the names of secondary columns to order by in addition t...
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.
mixed $mPastTheEndIndex
mixed $mFirstShown
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:42
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:32
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.
$queries
$last
const DB_REPLICA
Definition defines.php:25