mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
FEATURE: Added PaginatedList, which wraps around a data list or set to provide pagination functionality. This replaces the pagination functionality baked into DataObjectSet.
API CHANGE: Removed pagination related methods from DataObjectSet and implemented them on PaginatedList. API CHANGE: Removed DataObjectSet::parseQueryLimit(), this is now implemented as PaginatedList::setPaginationFromQuery(). API CHANGE: Deprecated DataObjectSet::TotalItems in favour of Count(). ENHANCEMENT: Added FirstLink and LastLink to PaginatedList. MINOR: Updated documentation, and added a how-to on paginating items.
This commit is contained in:
parent
79cde9df25
commit
3fbb29a6c5
416
core/PaginatedList.php
Normal file
416
core/PaginatedList.php
Normal file
@ -0,0 +1,416 @@
|
||||
<?php
|
||||
/**
|
||||
* A decorator that wraps around a data list in order to provide pagination.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage view
|
||||
*/
|
||||
class PaginatedList extends ViewableData implements IteratorAggregate {
|
||||
|
||||
protected $list;
|
||||
protected $request;
|
||||
protected $getVar = 'start';
|
||||
|
||||
protected $pageLength = 10;
|
||||
protected $pageStart;
|
||||
protected $totalItems;
|
||||
|
||||
/**
|
||||
* Constructs a new paginated list instance around a list.
|
||||
*
|
||||
* @param DataObjectSet $list The list to paginate. The getRange method will
|
||||
* be used to get the subset of objects to show.
|
||||
* @param array|ArrayAccess Either a map of request parameters or
|
||||
* request object that the pagination offset is read from.
|
||||
*/
|
||||
public function __construct(DataObjectSet $list, $request = array()) {
|
||||
if (!is_array($request) && !$request instanceof ArrayAccess) {
|
||||
throw new Exception('The request must be readable as an array.');
|
||||
}
|
||||
|
||||
$this->list = $list;
|
||||
$this->failover = $list;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
public function getList() {
|
||||
return $this->list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the GET var that is used to set the page start. This defaults
|
||||
* to "start".
|
||||
*
|
||||
* If there is more than one paginated list on a page, it is neccesary to
|
||||
* set a different get var for each using {@link setPaginationGetVar()}.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPaginationGetVar() {
|
||||
return $this->getVar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the GET var used to set the page start.
|
||||
*
|
||||
* @param string $var
|
||||
*/
|
||||
public function setPaginationGetVar($var) {
|
||||
$this->getVar = $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of items displayed per page. This defaults to 10.
|
||||
*
|
||||
* @return int.
|
||||
*/
|
||||
public function getPageLength() {
|
||||
return $this->pageLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of items displayed per page.
|
||||
*
|
||||
* @param int $length
|
||||
*/
|
||||
public function setPageLength($length) {
|
||||
$this->pageLength = $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current page.
|
||||
*
|
||||
* @param int $page
|
||||
*/
|
||||
public function setCurrentPage($page) {
|
||||
$this->pageStart = ($page - 1) * $this->pageLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the item the current page starts at.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPageStart() {
|
||||
if ($this->pageStart === null) {
|
||||
if ($this->request && isset($this->request[$this->getVar])) {
|
||||
$this->pageStart = (int) $this->request[$this->getVar];
|
||||
} else {
|
||||
$this->pageStart = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the item that current page starts at. This should be
|
||||
* a multiple of the page length.
|
||||
*
|
||||
* @param int $start
|
||||
*/
|
||||
public function setPageStart($start) {
|
||||
$this->pageStart = $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the unpaginated list.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalItems() {
|
||||
if ($this->totalItems === null) {
|
||||
$this->totalItems = count($this->list);
|
||||
}
|
||||
|
||||
return $this->totalItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of items in the list. This is useful when doing
|
||||
* custom pagination.
|
||||
*
|
||||
* @param int $items
|
||||
*/
|
||||
public function setTotalItems($items) {
|
||||
$this->totalItems = $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the page length, page start and total items from a query object's
|
||||
* limit, offset and unlimited count. The query MUST have a limit clause.
|
||||
*
|
||||
* @param SQLQuery $query
|
||||
*/
|
||||
public function setPaginationFromQuery(SQLQuery $query) {
|
||||
if ($query->limit) {
|
||||
$this->setPageLength($query->limit['limit']);
|
||||
$this->setPageStart($query->limit['start']);
|
||||
$this->setTotalItems($query->unlimitedRowCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IteratorIterator
|
||||
*/
|
||||
public function getIterator() {
|
||||
return new IteratorIterator(
|
||||
$this->list->getRange($this->getPageStart(), $this->pageLength)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of links to all the pages in the list. This is useful for
|
||||
* basic pagination.
|
||||
*
|
||||
* By default it returns links to every page, but if you pass the $max
|
||||
* parameter the number of pages will be limited to that number, centered
|
||||
* around the current page.
|
||||
*
|
||||
* @param int $max
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
public function Pages($max = null) {
|
||||
$result = new DataObjectSet();
|
||||
|
||||
if ($max) {
|
||||
$start = ($this->CurrentPage() - floor($max / 2)) - 1;
|
||||
$end = $this->CurrentPage() + floor($max / 2);
|
||||
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
$end = $max;
|
||||
}
|
||||
|
||||
if ($end > $this->TotalPages()) {
|
||||
$end = $this->TotalPages();
|
||||
$start = max(0, $end - $max);
|
||||
}
|
||||
} else {
|
||||
$start = 0;
|
||||
$end = $this->TotalPages();
|
||||
}
|
||||
|
||||
for ($i = $start; $i < $end; $i++) {
|
||||
$result->push(new ArrayData(array(
|
||||
'PageNum' => $i + 1,
|
||||
'Link' => HTTP::setGetVar($this->getVar, $i * $this->pageLength),
|
||||
'CurrentBool' => $this->CurrentPage() == ($i + 1)
|
||||
)));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a summarised pagination which limits the number of pages shown
|
||||
* around the current page for visually balanced.
|
||||
*
|
||||
* Example: 25 pages total, currently on page 6, context of 4 pages
|
||||
* [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next]
|
||||
*
|
||||
* Example template usage:
|
||||
* <code>
|
||||
* <% if MyPages.MoreThanOnePage %>
|
||||
* <% if MyPages.NotFirstPage %>
|
||||
* <a class="prev" href="$MyPages.PrevLink">Prev</a>
|
||||
* <% end_if %>
|
||||
* <% control MyPages.PaginationSummary(4) %>
|
||||
* <% if CurrentBool %>
|
||||
* $PageNum
|
||||
* <% else %>
|
||||
* <% if Link %>
|
||||
* <a href="$Link">$PageNum</a>
|
||||
* <% else %>
|
||||
* ...
|
||||
* <% end_if %>
|
||||
* <% end_if %>
|
||||
* <% end_control %>
|
||||
* <% if MyPages.NotLastPage %>
|
||||
* <a class="next" href="$MyPages.NextLink">Next</a>
|
||||
* <% end_if %>
|
||||
* <% end_if %>
|
||||
* </code>
|
||||
*
|
||||
* @param int $context The number of pages to display around the current
|
||||
* page. The number should be event, as half the number of each pages
|
||||
* are displayed on either side of the current one.
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
public function PaginationSummary($context = 4) {
|
||||
$result = new DataObjectSet();
|
||||
$current = $this->CurrentPage();
|
||||
$total = $this->TotalPages();
|
||||
|
||||
// Make the number even for offset calculations.
|
||||
if ($context % 2) {
|
||||
$context--;
|
||||
}
|
||||
|
||||
// If the first or last page is current, then show all context on one
|
||||
// side of it - otherwise show half on both sides.
|
||||
if ($current == 1 || $current == $total) {
|
||||
$offset = $context;
|
||||
} else {
|
||||
$offset = floor($context / 2);
|
||||
}
|
||||
|
||||
$left = max($current - $offset, 1);
|
||||
$range = range($current - $offset, $current + $offset);
|
||||
|
||||
if ($left + $context > $total) {
|
||||
$left = $total - $context;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$link = HTTP::setGetVar($this->getVar, $i * $this->pageLength);
|
||||
$num = $i + 1;
|
||||
|
||||
$emptyRange = $num != 1 && $num != $total && (
|
||||
$num == $left - 1 || $num == $left + $context + 1
|
||||
);
|
||||
|
||||
if ($emptyRange) {
|
||||
$result->push(new ArrayData(array(
|
||||
'PageNum' => null,
|
||||
'Link' => null,
|
||||
'CurrentBool' => false
|
||||
)));
|
||||
} elseif ($num == 1 || $num == $total || in_array($num, $range)) {
|
||||
$result->push(new ArrayData(array(
|
||||
'PageNum' => $num,
|
||||
'Link' => $link,
|
||||
'CurrentBool' => $current == $num
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function CurrentPage() {
|
||||
return floor($this->getPageStart() / $this->pageLength) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function TotalPages() {
|
||||
return ceil($this->getTotalItems() / $this->pageLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function MoreThanOnePage() {
|
||||
return $this->TotalPages() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function NotFirstPage() {
|
||||
return $this->CurrentPage() != 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function NotLastPage() {
|
||||
return $this->CurrentPage() != $this->TotalPages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of the first item being displayed on the current
|
||||
* page. This is useful for things like "displaying 10-20".
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function FirstItem() {
|
||||
return ($start = $this->getPageStart()) ? $start + 1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of the last item being displayed on this page.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function LastItem() {
|
||||
if ($start = $this->getPageStart()) {
|
||||
return min($start + $this->pageLength, $this->getTotalItems());
|
||||
} else {
|
||||
return min($this->pageLength, $this->getTotalItems());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to the first page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function FirstLink() {
|
||||
return HTTP::setGetVar($this->getVar, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to the last page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function LastLink() {
|
||||
return HTTP::setGetVar($this->getVar, ($this->TotalPages() - 1) * $this->pageLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to the next page, if there is another page after the
|
||||
* current one.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function NextLink() {
|
||||
if ($this->NotLastPage()) {
|
||||
return HTTP::setGetVar($this->getVar, $this->getPageStart() + $this->pageLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to the previous page, if the first page is not currently
|
||||
* active.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function PrevLink() {
|
||||
if ($this->NotFirstPage()) {
|
||||
return HTTP::setGetVar($this->getVar, $this->getPageStart() - $this->pageLength);
|
||||
}
|
||||
}
|
||||
|
||||
// DEPRECATED --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @deprecated 3.0 Use individual getter methods.
|
||||
*/
|
||||
public function getPageLimits() {
|
||||
return array(
|
||||
'pageStart' => $this->getPageStart(),
|
||||
'pageLength' => $this->pageLength,
|
||||
'totalSize' => $this->getTotalItems(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0 Use individual setter methods.
|
||||
*/
|
||||
public function setPageLimits($pageStart, $pageLength, $totalSize) {
|
||||
$this->setPageStart($pageStart);
|
||||
$this->setPageLength($pageLength);
|
||||
$this->setTotalSize($totalSize);
|
||||
}
|
||||
|
||||
}
|
@ -122,7 +122,6 @@ class DataList extends DataObjectSet {
|
||||
*/
|
||||
protected function generateItems() {
|
||||
$query = $this->dataQuery->query();
|
||||
$this->parseQueryLimit($query);
|
||||
$rows = $query->execute();
|
||||
$results = array();
|
||||
foreach($rows as $row) {
|
||||
|
66
docs/en/howto/pagination.md
Normal file
66
docs/en/howto/pagination.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Paginating A List
|
||||
|
||||
Adding pagination to a `[api:DataList]` or `[DataObjectSet]` is quite simple. All
|
||||
you need to do is wrap the object in a `[api:PaginatedList]` decorator, which takes
|
||||
care of fetching a sub-set of the total list and presenting it to the template.
|
||||
|
||||
In order to create a paginated list, you can create a method on your controller
|
||||
that first creates a `DataList` that will return all pages, and then wraps it
|
||||
in a `[api:PaginatedSet]` object. The `PaginatedList` object is also passed the
|
||||
HTTP request object so it can read the current page information from the
|
||||
"?start=" GET var.
|
||||
|
||||
The paginator will automatically set up query limits and read the request for
|
||||
information.
|
||||
|
||||
:::php
|
||||
/**
|
||||
* Returns a paginated list of all pages in the site.
|
||||
*/
|
||||
public function PaginatedPages() {
|
||||
$pages = DataList::create('Page');
|
||||
return new PaginatedList($pages, $this->request);
|
||||
}
|
||||
|
||||
## Setting Up The Template
|
||||
|
||||
Now all that remains is to render this list into a template, along with pagination
|
||||
controls. There are two ways to generate pagination controls:
|
||||
`[api:PaginatedSet->Pages()]` and `[api:PaginatedSet->PaginationSummary()]`. In
|
||||
this example we will use `PaginationSummary()`.
|
||||
|
||||
The first step is to simply list the objects in the template:
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% control PaginatedPages %>
|
||||
<li><a href="$Link">$Title</a></li>
|
||||
<% end_control %>
|
||||
</ul>
|
||||
|
||||
By default this will display 10 pages at a time. The next step is to add pagination
|
||||
controls below this so the user can switch between pages:
|
||||
|
||||
:::ss
|
||||
<% if PaginatedPages.MoreThanOnePage %>
|
||||
<% if PaginatedPages.NotFirstPage %>
|
||||
<a class="prev" href="$PaginatedPages.PrevLink">Prev</a>
|
||||
<% end_if %>
|
||||
<% control PaginatedPages.Pages %>
|
||||
<% if CurrentBool %>
|
||||
$PageNum
|
||||
<% else %>
|
||||
<% if Link %>
|
||||
<a href="$Link">$PageNum</a>
|
||||
<% else %>
|
||||
...
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
<% end_control %>
|
||||
<% if PaginatedPages.NotLastPage %>
|
||||
<a class="next" href="$PaginatedPages.NextLink">Next</a>
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
|
||||
If there is more than one page, this block will render a set of pagination
|
||||
controls in the form `[1] ... [3] [4] [[5]] [6] [7] ... [10]`.
|
@ -319,7 +319,7 @@ a quick reference (not all of them are described above):
|
||||
$NexPageLink, $Link, $RelativeLink, $ChildrenOf, $Page, $Level, $Menu, $Section2, $LoginForm, $SilverStripeNavigator,
|
||||
$PageComments, $Now, $LinkTo, $AbsoluteLink, $CurrentMember, $PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val,
|
||||
$JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString, $Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems,
|
||||
$BaseHref, $Debug, $CurrentPage, $Top
|
||||
$BaseHref, $Debug, $Top
|
||||
|
||||
### All fields available in Page_Controller
|
||||
|
||||
@ -334,7 +334,7 @@ $LinkToID, $VersionID, $CopyContentFromID, $RecordClassName
|
||||
$Link, $LinkOrCurrent, $LinkOrSection, $LinkingMode, $ElementName, $InSection, $Comments, $Breadcrumbs, $NestedTitle,
|
||||
$MetaTags, $ContentSource, $MultipleParents, $TreeTitle, $CMSTreeClasses, $Now, $LinkTo, $AbsoluteLink, $CurrentMember,
|
||||
$PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val, $JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString,
|
||||
$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $CurrentPage, $Top
|
||||
$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $Top
|
||||
|
||||
### All fields available in Page
|
||||
|
||||
|
@ -89,21 +89,25 @@ method, we're building our own `getCustomSearchContext()` variant.
|
||||
|
||||
### Pagination
|
||||
|
||||
For paginating records on multiple pages, you need to get the generated `SQLQuery` before firing off the actual
|
||||
search. This way we can set the "page limits" on the result through `setPageLimits()`, and only retrieve a fraction of
|
||||
the whole result set.
|
||||
|
||||
For pagination records on multiple pages, you need to wrap the results in a
|
||||
`PaginatedList` object. This object is also passed the generated `SQLQuery`
|
||||
in order to read page limit information. It is also passed the current
|
||||
`SS_HTTPRequest` object so it can read the current page from a GET var.
|
||||
|
||||
:::php
|
||||
function getResults($searchCriteria = array()) {
|
||||
public function getResults($searchCriteria = array()) {
|
||||
$start = ($this->request->getVar('start')) ? (int)$this->request->getVar('start') : 0;
|
||||
$limit = 10;
|
||||
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$query = $context->getQuery($searchCriteria, null, array('start'=>$start,'limit'=>$limit));
|
||||
$records = $context->getResults($searchCriteria, null, array('start'=>$start,'limit'=>$limit));
|
||||
|
||||
if($records) {
|
||||
$records->setPageLimits($start, $limit, $query->unlimitedRowCount());
|
||||
$records = new PaginatedList($records, $this->request);
|
||||
$records->setPageStart($start);
|
||||
$records->setPageSize($limit);
|
||||
$records->setTotalSize($query->unlimitedRowCount());
|
||||
}
|
||||
|
||||
return $records;
|
||||
@ -135,7 +139,7 @@ For more information on how to paginate your results within the template, see [T
|
||||
to show the results of your custom search you need at least this content in your template, notice that
|
||||
Results.PaginationSummary(4) defines how many pages the search will show in the search results. something like:
|
||||
|
||||
**Next 1 2 *3* 4 5 … 558**
|
||||
**Next 1 2 *3* 4 5 <EFBFBD><EFBFBD><EFBFBD> 558**
|
||||
|
||||
|
||||
:::ss
|
||||
|
@ -714,7 +714,6 @@ class File extends DataObject {
|
||||
|
||||
$records = $query->execute();
|
||||
$ret = $this->buildDataObjectSet($records, $containerClass);
|
||||
if($ret) $ret->parseQueryLimit($query);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
@ -32,30 +32,6 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
*/
|
||||
protected $current = null;
|
||||
|
||||
/**
|
||||
* The number object the current page starts at.
|
||||
* @var int
|
||||
*/
|
||||
protected $pageStart;
|
||||
|
||||
/**
|
||||
* The number of objects per page.
|
||||
* @var int
|
||||
*/
|
||||
protected $pageLength;
|
||||
|
||||
/**
|
||||
* Total number of DataObjects in this set.
|
||||
* @var int
|
||||
*/
|
||||
protected $totalSize;
|
||||
|
||||
/**
|
||||
* The pagination GET variable that controls the start of this set.
|
||||
* @var string
|
||||
*/
|
||||
protected $paginationGetVar = "start";
|
||||
|
||||
/**
|
||||
* Create a new DataObjectSet. If you pass one or more arguments, it will try to convert them into {@link ArrayData} objects.
|
||||
* @todo Does NOT automatically convert objects with complex datatypes (e.g. converting arrays within an objects to its own DataObjectSet)
|
||||
@ -189,272 +165,6 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
return $this->map($index, $titleField, $emptyString, $sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set number of objects on each page.
|
||||
* @param int $length Number of objects per page
|
||||
*/
|
||||
public function setPageLength($length) {
|
||||
$this->pageLength = $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page limits.
|
||||
* @param int $pageStart The start of this page.
|
||||
* @param int $pageLength Number of objects per page
|
||||
* @param int $totalSize Total number of objects.
|
||||
*/
|
||||
public function setPageLimits($pageStart, $pageLength, $totalSize) {
|
||||
$this->pageStart = $pageStart;
|
||||
$this->pageLength = $pageLength;
|
||||
$this->totalSize = $totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page limits
|
||||
* @return array
|
||||
*/
|
||||
public function getPageLimits() {
|
||||
return array(
|
||||
'pageStart' => $this->pageStart,
|
||||
'pageLength' => $this->pageLength,
|
||||
'totalSize' => $this->totalSize,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the limit from the given query to add prev/next buttons to this DataObjectSet.
|
||||
* @param SQLQuery $query The query used to generate this DataObjectSet
|
||||
*/
|
||||
public function parseQueryLimit(SQLQuery $query) {
|
||||
if($query->limit) {
|
||||
if(is_array($query->limit)) {
|
||||
$length = $query->limit['limit'];
|
||||
$start = $query->limit['start'];
|
||||
} else if(stripos($query->limit, 'OFFSET')) {
|
||||
list($length, $start) = preg_split("/ +OFFSET +/i", trim($query->limit));
|
||||
} else {
|
||||
$result = preg_split("/ *, */", trim($query->limit));
|
||||
$start = $result[0];
|
||||
$length = isset($result[1]) ? $result[1] : null;
|
||||
}
|
||||
|
||||
if(!$length) {
|
||||
$length = $start;
|
||||
$start = 0;
|
||||
}
|
||||
$this->setPageLimits($start, $length, $query->unlimitedRowCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of the current page.
|
||||
* @return int
|
||||
*/
|
||||
public function CurrentPage() {
|
||||
return floor($this->pageStart / $this->pageLength) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of pages.
|
||||
* @return int
|
||||
*/
|
||||
public function TotalPages() {
|
||||
if($this->totalSize == 0) {
|
||||
$this->totalSize = $this->Count();
|
||||
}
|
||||
if($this->pageLength == 0) {
|
||||
$this->pageLength = 10;
|
||||
}
|
||||
|
||||
return ceil($this->totalSize / $this->pageLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datafeed of page-links, good for use in search results, etc.
|
||||
* $maxPages will put an upper limit on the number of pages to return. It will
|
||||
* show the pages surrounding the current page, so you can still get to the deeper pages.
|
||||
* @param int $maxPages The maximum number of pages to return
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
public function Pages($maxPages = 0){
|
||||
$ret = new DataObjectSet();
|
||||
|
||||
if($maxPages) {
|
||||
$startPage = ($this->CurrentPage() - floor($maxPages / 2)) - 1;
|
||||
$endPage = $this->CurrentPage() + floor($maxPages / 2);
|
||||
|
||||
if($startPage < 0) {
|
||||
$startPage = 0;
|
||||
$endPage = $maxPages;
|
||||
}
|
||||
if($endPage > $this->TotalPages()) {
|
||||
$endPage = $this->TotalPages();
|
||||
$startPage = max(0, $endPage - $maxPages);
|
||||
}
|
||||
|
||||
} else {
|
||||
$startPage = 0;
|
||||
$endPage = $this->TotalPages();
|
||||
}
|
||||
|
||||
for($i=$startPage; $i < $endPage; $i++){
|
||||
$link = HTTP::setGetVar($this->paginationGetVar, $i*$this->pageLength);
|
||||
$thePage = new ArrayData(array(
|
||||
"PageNum" => $i+1,
|
||||
"Link" => $link,
|
||||
"CurrentBool" => ($this->CurrentPage() == $i+1)?true:false,
|
||||
)
|
||||
);
|
||||
$ret->push($thePage);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Display a summarized pagination which limits the number of pages shown
|
||||
* "around" the currently active page for visual balance.
|
||||
* In case more paginated pages have to be displayed, only
|
||||
*
|
||||
* Example: 25 pages total, currently on page 6, context of 4 pages
|
||||
* [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next]
|
||||
*
|
||||
* Example template usage:
|
||||
* <code>
|
||||
* <% if MyPages.MoreThanOnePage %>
|
||||
* <% if MyPages.NotFirstPage %>
|
||||
* <a class="prev" href="$MyPages.PrevLink">Prev</a>
|
||||
* <% end_if %>
|
||||
* <% control MyPages.PaginationSummary(4) %>
|
||||
* <% if CurrentBool %>
|
||||
* $PageNum
|
||||
* <% else %>
|
||||
* <% if Link %>
|
||||
* <a href="$Link">$PageNum</a>
|
||||
* <% else %>
|
||||
* ...
|
||||
* <% end_if %>
|
||||
* <% end_if %>
|
||||
* <% end_control %>
|
||||
* <% if MyPages.NotLastPage %>
|
||||
* <a class="next" href="$MyPages.NextLink">Next</a>
|
||||
* <% end_if %>
|
||||
* <% end_if %>
|
||||
* </code>
|
||||
*
|
||||
* @param integer $context Number of pages to display "around" the current page. Number should be even,
|
||||
* because its halved to either side of the current page.
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
public function PaginationSummary($context = 4) {
|
||||
$ret = new DataObjectSet();
|
||||
|
||||
// convert number of pages to even number for offset calculation
|
||||
if($context % 2) $context--;
|
||||
|
||||
// find out the offset
|
||||
$current = $this->CurrentPage();
|
||||
$totalPages = $this->TotalPages();
|
||||
|
||||
// if the first or last page is shown, use all content on one side (either left or right of current page)
|
||||
// otherwise half the number for usage "around" the current page
|
||||
$offset = ($current == 1 || $current == $totalPages) ? $context : floor($context/2);
|
||||
|
||||
$leftOffset = $current - ($offset);
|
||||
if($leftOffset < 1) $leftOffset = 1;
|
||||
if($leftOffset + $context > $totalPages) $leftOffset = $totalPages - $context;
|
||||
|
||||
for($i=0; $i < $totalPages; $i++) {
|
||||
$link = HTTP::setGetVar($this->paginationGetVar, $i*$this->pageLength);
|
||||
$num = $i+1;
|
||||
$currentBool = ($current == $i+1) ? true:false;
|
||||
if(
|
||||
($num == $leftOffset-1 && $num != 1 && $num != $totalPages)
|
||||
|| ($num == $leftOffset+$context+1 && $num != 1 && $num != $totalPages)
|
||||
) {
|
||||
$ret->push(new ArrayData(array(
|
||||
"PageNum" => null,
|
||||
"Link" => null,
|
||||
"CurrentBool" => $currentBool,
|
||||
)
|
||||
));
|
||||
} else if($num == 1 || $num == $totalPages || in_array($num, range($current-$offset,$current+$offset))) {
|
||||
$ret->push(new ArrayData(array(
|
||||
"PageNum" => $num,
|
||||
"Link" => $link,
|
||||
"CurrentBool" => $currentBool,
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current page is not the first page.
|
||||
* @return boolean
|
||||
*/
|
||||
public function NotFirstPage(){
|
||||
return $this->CurrentPage() != 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current page is not the last page.
|
||||
* @return boolean
|
||||
*/
|
||||
public function NotLastPage(){
|
||||
return $this->CurrentPage() != $this->TotalPages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is more than one page.
|
||||
* @return boolean
|
||||
*/
|
||||
public function MoreThanOnePage(){
|
||||
return $this->TotalPages() > 1;
|
||||
}
|
||||
|
||||
function FirstItem() {
|
||||
return isset($this->pageStart) ? $this->pageStart + 1 : 1;
|
||||
}
|
||||
|
||||
function LastItem() {
|
||||
if(isset($this->pageStart)) {
|
||||
return min($this->pageStart + $this->pageLength, $this->totalSize);
|
||||
} else {
|
||||
return min($this->pageLength, $this->totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the previous page.
|
||||
* @return string
|
||||
*/
|
||||
public function PrevLink() {
|
||||
if($this->pageStart - $this->pageLength >= 0) {
|
||||
return HTTP::setGetVar($this->paginationGetVar, $this->pageStart - $this->pageLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the next page.
|
||||
* @return string
|
||||
*/
|
||||
public function NextLink() {
|
||||
if($this->pageStart + $this->pageLength < $this->totalSize) {
|
||||
return HTTP::setGetVar($this->paginationGetVar, $this->pageStart + $this->pageLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows us to use multiple pagination GET variables on the same page (eg. if you have search results and page comments on a single page)
|
||||
*
|
||||
* @param string $var The variable to go in the GET string (Defaults to 'start')
|
||||
*/
|
||||
public function setPaginationGetVar($var) {
|
||||
$this->paginationGetVar = $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the DataObject Set.
|
||||
* @param DataObject $item Item to add.
|
||||
@ -608,11 +318,10 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total number of items in this dataset.
|
||||
* @return int
|
||||
* @deprecated 3.0 Use {@link DataObjectSet::Count()}.
|
||||
*/
|
||||
public function TotalItems() {
|
||||
return $this->totalSize ? $this->totalSize : $this->Count();
|
||||
return $this->Count();
|
||||
}
|
||||
|
||||
/**
|
||||
|
247
tests/PaginatedListTest.php
Normal file
247
tests/PaginatedListTest.php
Normal file
@ -0,0 +1,247 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for the {@link PaginatedList} class.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage tests
|
||||
*/
|
||||
class PaginatedListTest extends SapphireTest {
|
||||
|
||||
public function testPageStart() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$this->assertEquals(0, $list->getPageStart(), 'The start defaults to 0.');
|
||||
|
||||
$list->setPageStart(10);
|
||||
$this->assertEquals(10, $list->getPageStart(), 'You can set the page start.');
|
||||
|
||||
$list = new PaginatedList(new DataObjectSet(), array('start' => 50));
|
||||
$this->assertEquals(50, $list->getPageStart(), 'The page start can be read from the request.');
|
||||
}
|
||||
|
||||
public function testGetTotalItems() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$this->assertEquals(0, $list->getTotalItems());
|
||||
|
||||
$list->setTotalItems(10);
|
||||
$this->assertEquals(10, $list->getTotalItems());
|
||||
|
||||
$list = new PaginatedList(new DataObjectSet(
|
||||
new ArrayData(array()),
|
||||
new ArrayData(array())
|
||||
));
|
||||
$this->assertEquals(2, $list->getTotalItems());
|
||||
}
|
||||
|
||||
public function testSetPaginationFromQuery() {
|
||||
$query = $this->getMock('SQLQuery');
|
||||
$query->limit = array('limit' => 15, 'start' => 30);
|
||||
$query->expects($this->once())
|
||||
->method('unlimitedRowCount')
|
||||
->will($this->returnValue(100));
|
||||
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setPaginationFromQuery($query);
|
||||
|
||||
$this->assertEquals(15, $list->getPageLength());
|
||||
$this->assertEquals(30, $list->getPageStart());
|
||||
$this->assertEquals(100, $list->getTotalItems());
|
||||
}
|
||||
|
||||
public function testSetCurrentPage() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setPageLength(10);
|
||||
$list->setCurrentPage(10);
|
||||
|
||||
$this->assertEquals(10, $list->CurrentPage());
|
||||
$this->assertEquals(90, $list->getPageStart());
|
||||
}
|
||||
|
||||
public function testGetIterator() {
|
||||
$list = new PaginatedList(new DataObjectSet(array(
|
||||
new DataObject(array('Num' => 1)),
|
||||
new DataObject(array('Num' => 2)),
|
||||
new DataObject(array('Num' => 3)),
|
||||
new DataObject(array('Num' => 4)),
|
||||
new DataObject(array('Num' => 5)),
|
||||
)));
|
||||
$list->setPageLength(2);
|
||||
|
||||
$this->assertDOSEquals(
|
||||
array(array('Num' => 1), array('Num' => 2)), $list->getIterator()
|
||||
);
|
||||
|
||||
$list->setCurrentPage(2);
|
||||
$this->assertDOSEquals(
|
||||
array(array('Num' => 3), array('Num' => 4)), $list->getIterator()
|
||||
);
|
||||
|
||||
$list->setCurrentPage(3);
|
||||
$this->assertDOSEquals(
|
||||
array(array('Num' => 5)), $list->getIterator()
|
||||
);
|
||||
|
||||
$list->setCurrentPage(999);
|
||||
$this->assertDOSEquals(array(), $list->getIterator());
|
||||
}
|
||||
|
||||
public function testPages() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setPageLength(10);
|
||||
$list->setTotalItems(50);
|
||||
|
||||
$this->assertEquals(5, count($list->Pages()));
|
||||
$this->assertEquals(3, count($list->Pages(3)));
|
||||
$this->assertEquals(5, count($list->Pages(15)));
|
||||
|
||||
$list->setCurrentPage(3);
|
||||
|
||||
$expectAll = array(
|
||||
array('PageNum' => 1),
|
||||
array('PageNum' => 2),
|
||||
array('PageNum' => 3, 'CurrentBool' => true),
|
||||
array('PageNum' => 4),
|
||||
array('PageNum' => 5),
|
||||
);
|
||||
$this->assertDOSEquals($expectAll, $list->Pages());
|
||||
|
||||
$expectLimited = array(
|
||||
array('PageNum' => 2),
|
||||
array('PageNum' => 3, 'CurrentBool' => true),
|
||||
array('PageNum' => 4),
|
||||
);
|
||||
$this->assertDOSEquals($expectLimited, $list->Pages(3));
|
||||
}
|
||||
|
||||
public function testPaginationSummary() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
|
||||
$list->setPageLength(10);
|
||||
$list->setTotalItems(250);
|
||||
$list->setCurrentPage(6);
|
||||
|
||||
$expect = array(
|
||||
array('PageNum' => 1),
|
||||
array('PageNum' => null),
|
||||
array('PageNum' => 4),
|
||||
array('PageNum' => 5),
|
||||
array('PageNum' => 6, 'CurrentBool' => true),
|
||||
array('PageNum' => 7),
|
||||
array('PageNum' => 8),
|
||||
array('PageNum' => null),
|
||||
array('PageNum' => 25),
|
||||
);
|
||||
$this->assertDOSEquals($expect, $list->PaginationSummary(4));
|
||||
}
|
||||
|
||||
public function testCurrentPage() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setTotalItems(50);
|
||||
|
||||
$this->assertEquals(1, $list->CurrentPage());
|
||||
$list->setPageStart(10);
|
||||
$this->assertEquals(2, $list->CurrentPage());
|
||||
$list->setPageStart(40);
|
||||
$this->assertEquals(5, $list->CurrentPage());
|
||||
}
|
||||
|
||||
public function testTotalPages() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
|
||||
$list->setPageLength(1);
|
||||
$this->assertEquals(0, $list->TotalPages());
|
||||
|
||||
$list->setTotalItems(1);
|
||||
$this->assertEquals(1, $list->TotalPages());
|
||||
|
||||
$list->setTotalItems(5);
|
||||
$this->assertEquals(5, $list->TotalPages());
|
||||
}
|
||||
|
||||
public function testMoreThanOnePage() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
|
||||
$list->setPageLength(1);
|
||||
$list->setTotalItems(1);
|
||||
$this->assertFalse($list->MoreThanOnePage());
|
||||
|
||||
$list->setTotalItems(2);
|
||||
$this->assertTrue($list->MoreThanOnePage());
|
||||
}
|
||||
|
||||
public function testNotFirstPage() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$this->assertFalse($list->NotFirstPage());
|
||||
$list->setCurrentPage(2);
|
||||
$this->assertTrue($list->NotFirstPage());
|
||||
}
|
||||
|
||||
public function testNotLastPage() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setTotalItems(50);
|
||||
|
||||
$this->assertTrue($list->NotLastPage());
|
||||
$list->setCurrentPage(5);
|
||||
$this->assertFalse($list->NotLastPage());
|
||||
}
|
||||
|
||||
public function testFirstItem() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$this->assertEquals(1, $list->FirstItem());
|
||||
$list->setPageStart(10);
|
||||
$this->assertEquals(11, $list->FirstItem());
|
||||
}
|
||||
|
||||
public function testLastItem() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setPageLength(10);
|
||||
$list->setTotalItems(25);
|
||||
|
||||
$list->setCurrentPage(1);
|
||||
$this->assertEquals(10, $list->LastItem());
|
||||
$list->setCurrentPage(2);
|
||||
$this->assertEquals(20, $list->LastItem());
|
||||
$list->setCurrentPage(3);
|
||||
$this->assertEquals(25, $list->LastItem());
|
||||
}
|
||||
|
||||
public function testFirstLink() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$this->assertContains('start=0', $list->FirstLink());
|
||||
}
|
||||
|
||||
public function testLastLink() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setPageLength(10);
|
||||
$list->setTotalItems(100);
|
||||
$this->assertContains('start=90', $list->LastLink());
|
||||
}
|
||||
|
||||
public function testNextLink() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setTotalItems(50);
|
||||
|
||||
$this->assertContains('start=10', $list->NextLink());
|
||||
$list->setCurrentPage(2);
|
||||
$this->assertContains('start=20', $list->NextLink());
|
||||
$list->setCurrentPage(3);
|
||||
$this->assertContains('start=30', $list->NextLink());
|
||||
$list->setCurrentPage(4);
|
||||
$this->assertContains('start=40', $list->NextLink());
|
||||
$list->setCurrentPage(5);
|
||||
$this->assertNull($list->NextLink());
|
||||
}
|
||||
|
||||
public function testPrevLink() {
|
||||
$list = new PaginatedList(new DataObjectSet());
|
||||
$list->setTotalItems(50);
|
||||
|
||||
$this->assertNull($list->PrevLink());
|
||||
$list->setCurrentPage(2);
|
||||
$this->assertContains('start=0', $list->PrevLink());
|
||||
$list->setCurrentPage(3);
|
||||
$this->assertContains('start=10', $list->PrevLink());
|
||||
$list->setCurrentPage(5);
|
||||
$this->assertContains('start=30', $list->PrevLink());
|
||||
}
|
||||
|
||||
}
|
@ -245,52 +245,6 @@ class DataObjectSetTest extends SapphireTest {
|
||||
$this->assertEquals($mixedSet->Count(), 2, 'There are 3 unique data objects in a very mixed set');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test {@link DataObjectSet->parseQueryLimit()}
|
||||
*/
|
||||
function testParseQueryLimit() {
|
||||
// Create empty objects, because they don't need to have contents
|
||||
$sql = new SQLQuery('*', '"Member"');
|
||||
$max = $sql->unlimitedRowCount();
|
||||
$set = new DataObjectSet();
|
||||
|
||||
// Test handling an array
|
||||
$set->parseQueryLimit($sql->limit(array('limit'=>5, 'start'=>2)));
|
||||
$expected = array(
|
||||
'pageStart' => 2,
|
||||
'pageLength' => 5,
|
||||
'totalSize' => $max,
|
||||
);
|
||||
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
|
||||
|
||||
// Test handling OFFSET string
|
||||
// uppercase
|
||||
$set->parseQueryLimit($sql->limit('3 OFFSET 1'));
|
||||
$expected = array(
|
||||
'pageStart' => 1,
|
||||
'pageLength' => 3,
|
||||
'totalSize' => $max,
|
||||
);
|
||||
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
|
||||
// and lowercase
|
||||
$set->parseQueryLimit($sql->limit('32 offset 3'));
|
||||
$expected = array(
|
||||
'pageStart' => 3,
|
||||
'pageLength' => 32,
|
||||
'totalSize' => $max,
|
||||
);
|
||||
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
|
||||
|
||||
// Finally check MySQL LIMIT syntax
|
||||
$set->parseQueryLimit($sql->limit('7, 7'));
|
||||
$expected = array(
|
||||
'pageStart' => 7,
|
||||
'pageLength' => 7,
|
||||
'totalSize' => $max,
|
||||
);
|
||||
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test {@link DataObjectSet->insertFirst()}
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user