MediaWiki  REL1_24
IndexPager.php
Go to the documentation of this file.
00001 <?php
00066 abstract class IndexPager extends ContextSource implements Pager {
00072     const DIR_ASCENDING = false;
00073     const DIR_DESCENDING = true;
00074 
00075     public $mRequest;
00076     public $mLimitsShown = array( 20, 50, 100, 250, 500 );
00077     public $mDefaultLimit = 50;
00078     public $mOffset, $mLimit;
00079     public $mQueryDone = false;
00080     public $mDb;
00081     public $mPastTheEndRow;
00082 
00087     protected $mIndexField;
00092     protected $mExtraSortFields;
00095     protected $mOrderType;
00107     public $mDefaultDirection;
00108     public $mIsBackwards;
00109 
00111     public $mIsFirst;
00112     public $mIsLast;
00113 
00114     protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar;
00115 
00119     protected $mIncludeOffset = false;
00120 
00126     public $mResult;
00127 
00128     public function __construct( IContextSource $context = null ) {
00129         if ( $context ) {
00130             $this->setContext( $context );
00131         }
00132 
00133         $this->mRequest = $this->getRequest();
00134 
00135         # NB: the offset is quoted, not validated. It is treated as an
00136         # arbitrary string to support the widest variety of index types. Be
00137         # careful outputting it into HTML!
00138         $this->mOffset = $this->mRequest->getText( 'offset' );
00139 
00140         # Use consistent behavior for the limit options
00141         $this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' );
00142         if ( !$this->mLimit ) {
00143             // Don't override if a subclass calls $this->setLimit() in its constructor.
00144             list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
00145         }
00146 
00147         $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
00148         # Let the subclass set the DB here; otherwise use a slave DB for the current wiki
00149         $this->mDb = $this->mDb ?: wfGetDB( DB_SLAVE );
00150 
00151         $index = $this->getIndexField(); // column to sort on
00152         $extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning
00153         $order = $this->mRequest->getVal( 'order' );
00154         if ( is_array( $index ) && isset( $index[$order] ) ) {
00155             $this->mOrderType = $order;
00156             $this->mIndexField = $index[$order];
00157             $this->mExtraSortFields = isset( $extraSort[$order] )
00158                 ? (array)$extraSort[$order]
00159                 : array();
00160         } elseif ( is_array( $index ) ) {
00161             # First element is the default
00162             reset( $index );
00163             list( $this->mOrderType, $this->mIndexField ) = each( $index );
00164             $this->mExtraSortFields = isset( $extraSort[$this->mOrderType] )
00165                 ? (array)$extraSort[$this->mOrderType]
00166                 : array();
00167         } else {
00168             # $index is not an array
00169             $this->mOrderType = null;
00170             $this->mIndexField = $index;
00171             $this->mExtraSortFields = (array)$extraSort;
00172         }
00173 
00174         if ( !isset( $this->mDefaultDirection ) ) {
00175             $dir = $this->getDefaultDirections();
00176             $this->mDefaultDirection = is_array( $dir )
00177                 ? $dir[$this->mOrderType]
00178                 : $dir;
00179         }
00180     }
00181 
00187     public function getDatabase() {
00188         return $this->mDb;
00189     }
00190 
00196     public function doQuery() {
00197         # Use the child class name for profiling
00198         $fname = __METHOD__ . ' (' . get_class( $this ) . ')';
00199         wfProfileIn( $fname );
00200 
00201         // @todo This should probably compare to DIR_DESCENDING and DIR_ASCENDING constants
00202         $descending = ( $this->mIsBackwards == $this->mDefaultDirection );
00203         # Plus an extra row so that we can tell the "next" link should be shown
00204         $queryLimit = $this->mLimit + 1;
00205 
00206         if ( $this->mOffset == '' ) {
00207             $isFirst = true;
00208         } else {
00209             // If there's an offset, we may or may not be at the first entry.
00210             // The only way to tell is to run the query in the opposite
00211             // direction see if we get a row.
00212             $oldIncludeOffset = $this->mIncludeOffset;
00213             $this->mIncludeOffset = !$this->mIncludeOffset;
00214             $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, !$descending )->numRows();
00215             $this->mIncludeOffset = $oldIncludeOffset;
00216         }
00217 
00218         $this->mResult = $this->reallyDoQuery(
00219             $this->mOffset,
00220             $queryLimit,
00221             $descending
00222         );
00223 
00224         $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult );
00225         $this->mQueryDone = true;
00226 
00227         $this->preprocessResults( $this->mResult );
00228         $this->mResult->rewind(); // Paranoia
00229 
00230         wfProfileOut( $fname );
00231     }
00232 
00236     function getResult() {
00237         return $this->mResult;
00238     }
00239 
00245     function setOffset( $offset ) {
00246         $this->mOffset = $offset;
00247     }
00248 
00256     function setLimit( $limit ) {
00257         $limit = (int)$limit;
00258         // WebRequest::getLimitOffset() puts a cap of 5000, so do same here.
00259         if ( $limit > 5000 ) {
00260             $limit = 5000;
00261         }
00262         if ( $limit > 0 ) {
00263             $this->mLimit = $limit;
00264         }
00265     }
00266 
00272     function getLimit() {
00273         return $this->mLimit;
00274     }
00275 
00283     public function setIncludeOffset( $include ) {
00284         $this->mIncludeOffset = $include;
00285     }
00286 
00296     function extractResultInfo( $isFirst, $limit, ResultWrapper $res ) {
00297         $numRows = $res->numRows();
00298         if ( $numRows ) {
00299             # Remove any table prefix from index field
00300             $parts = explode( '.', $this->mIndexField );
00301             $indexColumn = end( $parts );
00302 
00303             $row = $res->fetchRow();
00304             $firstIndex = $row[$indexColumn];
00305 
00306             # Discard the extra result row if there is one
00307             if ( $numRows > $this->mLimit && $numRows > 1 ) {
00308                 $res->seek( $numRows - 1 );
00309                 $this->mPastTheEndRow = $res->fetchObject();
00310                 $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn;
00311                 $res->seek( $numRows - 2 );
00312                 $row = $res->fetchRow();
00313                 $lastIndex = $row[$indexColumn];
00314             } else {
00315                 $this->mPastTheEndRow = null;
00316                 # Setting indexes to an empty string means that they will be
00317                 # omitted if they would otherwise appear in URLs. It just so
00318                 # happens that this  is the right thing to do in the standard
00319                 # UI, in all the relevant cases.
00320                 $this->mPastTheEndIndex = '';
00321                 $res->seek( $numRows - 1 );
00322                 $row = $res->fetchRow();
00323                 $lastIndex = $row[$indexColumn];
00324             }
00325         } else {
00326             $firstIndex = '';
00327             $lastIndex = '';
00328             $this->mPastTheEndRow = null;
00329             $this->mPastTheEndIndex = '';
00330         }
00331 
00332         if ( $this->mIsBackwards ) {
00333             $this->mIsFirst = ( $numRows < $limit );
00334             $this->mIsLast = $isFirst;
00335             $this->mLastShown = $firstIndex;
00336             $this->mFirstShown = $lastIndex;
00337         } else {
00338             $this->mIsFirst = $isFirst;
00339             $this->mIsLast = ( $numRows < $limit );
00340             $this->mLastShown = $lastIndex;
00341             $this->mFirstShown = $firstIndex;
00342         }
00343     }
00344 
00350     function getSqlComment() {
00351         return get_class( $this );
00352     }
00353 
00363     public function reallyDoQuery( $offset, $limit, $descending ) {
00364         list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
00365             $this->buildQueryInfo( $offset, $limit, $descending );
00366 
00367         return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
00368     }
00369 
00378     protected function buildQueryInfo( $offset, $limit, $descending ) {
00379         $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
00380         $info = $this->getQueryInfo();
00381         $tables = $info['tables'];
00382         $fields = $info['fields'];
00383         $conds = isset( $info['conds'] ) ? $info['conds'] : array();
00384         $options = isset( $info['options'] ) ? $info['options'] : array();
00385         $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array();
00386         $sortColumns = array_merge( array( $this->mIndexField ), $this->mExtraSortFields );
00387         if ( $descending ) {
00388             $options['ORDER BY'] = $sortColumns;
00389             $operator = $this->mIncludeOffset ? '>=' : '>';
00390         } else {
00391             $orderBy = array();
00392             foreach ( $sortColumns as $col ) {
00393                 $orderBy[] = $col . ' DESC';
00394             }
00395             $options['ORDER BY'] = $orderBy;
00396             $operator = $this->mIncludeOffset ? '<=' : '<';
00397         }
00398         if ( $offset != '' ) {
00399             $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
00400         }
00401         $options['LIMIT'] = intval( $limit );
00402         return array( $tables, $fields, $conds, $fname, $options, $join_conds );
00403     }
00404 
00410     protected function preprocessResults( $result ) {
00411     }
00412 
00419     public function getBody() {
00420         if ( !$this->mQueryDone ) {
00421             $this->doQuery();
00422         }
00423 
00424         if ( $this->mResult->numRows() ) {
00425             # Do any special query batches before display
00426             $this->doBatchLookups();
00427         }
00428 
00429         # Don't use any extra rows returned by the query
00430         $numRows = min( $this->mResult->numRows(), $this->mLimit );
00431 
00432         $s = $this->getStartBody();
00433         if ( $numRows ) {
00434             if ( $this->mIsBackwards ) {
00435                 for ( $i = $numRows - 1; $i >= 0; $i-- ) {
00436                     $this->mResult->seek( $i );
00437                     $row = $this->mResult->fetchObject();
00438                     $s .= $this->formatRow( $row );
00439                 }
00440             } else {
00441                 $this->mResult->seek( 0 );
00442                 for ( $i = 0; $i < $numRows; $i++ ) {
00443                     $row = $this->mResult->fetchObject();
00444                     $s .= $this->formatRow( $row );
00445                 }
00446             }
00447         } else {
00448             $s .= $this->getEmptyBody();
00449         }
00450         $s .= $this->getEndBody();
00451         return $s;
00452     }
00453 
00463     function makeLink( $text, array $query = null, $type = null ) {
00464         if ( $query === null ) {
00465             return $text;
00466         }
00467 
00468         $attrs = array();
00469         if ( in_array( $type, array( 'prev', 'next' ) ) ) {
00470             $attrs['rel'] = $type;
00471         }
00472 
00473         if ( in_array( $type, array( 'asc', 'desc' ) ) ) {
00474             $attrs['title'] = wfMessage( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
00475         }
00476 
00477         if ( $type ) {
00478             $attrs['class'] = "mw-{$type}link";
00479         }
00480 
00481 
00482         return Linker::linkKnown(
00483             $this->getTitle(),
00484             $text,
00485             $attrs,
00486             $query + $this->getDefaultQuery()
00487         );
00488     }
00489 
00497     protected function doBatchLookups() {
00498     }
00499 
00506     protected function getStartBody() {
00507         return '';
00508     }
00509 
00515     protected function getEndBody() {
00516         return '';
00517     }
00518 
00525     protected function getEmptyBody() {
00526         return '';
00527     }
00528 
00536     function getDefaultQuery() {
00537         if ( !isset( $this->mDefaultQuery ) ) {
00538             $this->mDefaultQuery = $this->getRequest()->getQueryValues();
00539             unset( $this->mDefaultQuery['title'] );
00540             unset( $this->mDefaultQuery['dir'] );
00541             unset( $this->mDefaultQuery['offset'] );
00542             unset( $this->mDefaultQuery['limit'] );
00543             unset( $this->mDefaultQuery['order'] );
00544             unset( $this->mDefaultQuery['month'] );
00545             unset( $this->mDefaultQuery['year'] );
00546         }
00547         return $this->mDefaultQuery;
00548     }
00549 
00555     function getNumRows() {
00556         if ( !$this->mQueryDone ) {
00557             $this->doQuery();
00558         }
00559         return $this->mResult->numRows();
00560     }
00561 
00567     function getPagingQueries() {
00568         if ( !$this->mQueryDone ) {
00569             $this->doQuery();
00570         }
00571 
00572         # Don't announce the limit everywhere if it's the default
00573         $urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit;
00574 
00575         if ( $this->mIsFirst ) {
00576             $prev = false;
00577             $first = false;
00578         } else {
00579             $prev = array(
00580                 'dir' => 'prev',
00581                 'offset' => $this->mFirstShown,
00582                 'limit' => $urlLimit
00583             );
00584             $first = array( 'limit' => $urlLimit );
00585         }
00586         if ( $this->mIsLast ) {
00587             $next = false;
00588             $last = false;
00589         } else {
00590             $next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit );
00591             $last = array( 'dir' => 'prev', 'limit' => $urlLimit );
00592         }
00593         return array(
00594             'prev' => $prev,
00595             'next' => $next,
00596             'first' => $first,
00597             'last' => $last
00598         );
00599     }
00600 
00606     function isNavigationBarShown() {
00607         if ( !$this->mQueryDone ) {
00608             $this->doQuery();
00609         }
00610         // Hide navigation by default if there is nothing to page
00611         return !( $this->mIsFirst && $this->mIsLast );
00612     }
00613 
00624     function getPagingLinks( $linkTexts, $disabledTexts = array() ) {
00625         $queries = $this->getPagingQueries();
00626         $links = array();
00627 
00628         foreach ( $queries as $type => $query ) {
00629             if ( $query !== false ) {
00630                 $links[$type] = $this->makeLink(
00631                     $linkTexts[$type],
00632                     $queries[$type],
00633                     $type
00634                 );
00635             } elseif ( isset( $disabledTexts[$type] ) ) {
00636                 $links[$type] = $disabledTexts[$type];
00637             } else {
00638                 $links[$type] = $linkTexts[$type];
00639             }
00640         }
00641 
00642         return $links;
00643     }
00644 
00645     function getLimitLinks() {
00646         $links = array();
00647         if ( $this->mIsBackwards ) {
00648             $offset = $this->mPastTheEndIndex;
00649         } else {
00650             $offset = $this->mOffset;
00651         }
00652         foreach ( $this->mLimitsShown as $limit ) {
00653             $links[] = $this->makeLink(
00654                 $this->getLanguage()->formatNum( $limit ),
00655                 array( 'offset' => $offset, 'limit' => $limit ),
00656                 'num'
00657             );
00658         }
00659         return $links;
00660     }
00661 
00670     abstract function formatRow( $row );
00671 
00684     abstract function getQueryInfo();
00685 
00698     abstract function getIndexField();
00699 
00716     protected function getExtraSortFields() {
00717         return array();
00718     }
00719 
00739     protected function getDefaultDirections() {
00740         return IndexPager::DIR_ASCENDING;
00741     }
00742 }