Page 1 of 1

Skeleton Framework: Pagination Component - Please Review

Posted: Fri May 29, 2009 1:29 am
by allspiritseve
Arborint and I have been hard at work on a pagination component that can be used standalone or as part of the Skeleton framework. We're looking for feedback from the DevNetwork community, so take a look and let us know what you think.

Executive Summary
We made these classes first and foremost to make it drop-dead easy to paginate data. Our goal was not to make a one size fits all solution, but rather to provide a layered range of solutions in which developers can pick and choose to tailor the classes to their needs.

Specific Goals
  • Account for as many use cases as possible
  • Stay flexible for extendability
  • Take care of the messy details of pagination: url generation, maintaining state, dealing with the request.
Basics
The base object in this pagination system is a Value Object that holds the values for the list and does the necessary calculations. This class can be used alone, but you are required to initialize it.

Code: Select all

$pager = new A_Pagination_Core($datasource);
$pager->setCurrentPage(intval($_GET['mypagevar']));
$pager->setNumItems(intval($_GET['mycountvar']));
This component also provides a class that extends the Core class and initializes itself from the request so you don't have to. This class adds a process() method that will initialize the object for you:

Code: Select all

$pager = new A_Pagination_Request($datasource);
$pager->process();
This Core object does the necessary calculation and will return page numbers for the current first, last, next and previous pages, plus a range of page numbers around the current page. These are intended to provide the basic functionality on which to build classes to render paginated output.

This class takes a Datasource object passed to the constructor. These Datasources comply to a simple Adapter interface. This component will provide Adapters for arrays, PDO, MySQL, etc. A getItems() method is provided to retrieve the records for the currently displayed page.

The basic methods in its interface are:

Code: Select all

class A_Pagination_Core {
     function __construct($datasource)
     function isPage($number)
     function getPage($number)
     function getCurrentPage()
     function getFirstPage()
     function getLastPage()
     function getPageRange()
     function getNumItems()
     function getItems()
}
This component is not monolithic. There are several levels of classes that use a Core object and provide support for rendering output. The lowest level of output support uses page numbers from a Core object and builds URLs. Using this class you can have complete control of your template:

Code: Select all

$url = new A_Pagination_Helper_Url($pager);
$out .= '<a href="' . $url->previous() . '">Prev</> ";
$out .= '<a href="' . $url->next('Next') . '">Next</> ";
The next level of output support uses page numbers from a Core object and builds complete links (<a> tags). You an pass the name of a CSS class to this object to control link style:

Code: Select all

$url = new A_Pagination_Helper_Link($pager);
$out .= $link->previous('Prev', 'mylinkclass');
$out .= $link->next('Next', 'mylinkclass');
Use Case Example
Here is an example of combining the classes to do a basic paginated list with links:

Code: Select all

// create a data object that has the interface needed by the Pager object
$datasource = new Datasource();
 
// create a request processor to set pager from GET parameters
$pager = new A_Pagination_Request($datasource);
 
// initialize using values from $_GET
$pager->process();
 
// create a "standard" view object to create pagination links
include 'A/Pagination/View/Standard.php';
$view = new A_Pagination_View_Standard($pager);
 
// display the data
echo '<table border="1">';
foreach ($pager->getItems() as $row) {
    echo '<tr>';
    echo '<td>' . $row['id'] . '</td><td>' . $row['name'] . '</td>';
    echo '</tr>';
}
echo '</table>';
 
// display the pagination links
echo $view->render();
Additional Features
  • There are a number of examples that show different ways the classes can be used -- from do-it-yourself to standalone.
  • There is support for ORDER BY functionality to sort the list. There are methods to generate column heading links to sort any column ascending/descending. Sort order is maintained while paging.
  • The total number of items in the datasource is persisted RESTfully so, for example, COUNT() is only called one for database datasources.
  • Request parameter names can be changed to remove conflicts and allow multiple pagers on one page.
  • Pagination state values can be saved in the session if you want to be able to resume in the list where you left off -- when editing records in CRUD for example.
Feedback Wanted
  • Do these classes provide everything you need for pagination?
  • What modifications would you make before using this in a project?
  • What can we do to improve the code?
DataGrid
For the most automated use case, we will be providing a DataGrid that takes care of all HTML once the core classes have been finalized.

The code attachment is an older version

Latest code available at: http://skeleton.googlecode.com/

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Sat May 30, 2009 3:35 am
by matthijs
Great work guys. Looks good. My first feedback, just from a quick look at the examples, is that I'd like to have the sortby direction as well. So click on month for example, and it sorts by month ascending. Click again and you get sort descending

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Sat May 30, 2009 12:45 pm
by Christopher
matthijs wrote:My first feedback, just from a quick look at the examples, is that I'd like to have the sortby direction as well. So click on month for example, and it sorts by month ascending. Click again and you get sort descending
It is supposed to work that way. I just committed a change to fix that bug. I'll have allspiritseve update the downloads.

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Sat May 30, 2009 12:57 pm
by allspiritseve
Files updated.

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Sun May 31, 2009 9:47 pm
by Christopher
I hope that more people will take the time to download this code and try it out. We would really appreciate the feedback.

I know that people's first reaction is often- "What is all that code for? I don't get it?" We are trying to build a layered solution that allows people to pick the level they want to deal with. That is the reason why this component is built with a bunch of small, single purpose classes (besides that being a good practice ;)). The levels you can use are:

Page Numbers

Code: Select all

pager = new A_Pagination_Core($datasource);
echo $pager->getNextPage();
Output: 8

URL

Code: Select all

pager = new A_Pagination_Core($datasource);
$url = new A_Pagination_Helper_Url($pager);
echo $url->next();
Output: http://mysite.com/mypage.php?page=8

Links

Code: Select all

pager = new A_Pagination_Core($datasource);
$link = new A_Pagination_Helper_Link($pager);
echo $link->next();
Output: 8 (Note this is a link (<a>) to the URL above

Standard View Helper

Code: Select all

pager = new A_Pagination_Core($datasource);
$view = new A_Pagination_View_Standard($pager);
echo $view->render();
Output: First Prev 5 6 7 8 9 Next Last

All-in-one

Code: Select all

view = new A_Pagination_Standalone($datasource);
echo $view->render();
Output: First Prev 5 6 7 8 9 Next Last

We have also discussed possibly implementing different View Helpers to allow you to do pagination links like Google, phpBB, Flikr, etc. (e.g. A_Pagination_View_Google). All of the core functionality to build those is in the lower layers.

It is also our intention to provide a simple DataGrid. (And we may do a Ajax-ified, edit-in-place version as well)

Data Grid

Code: Select all

pager = new A_Pagination_Request($datasource);
$view = new A_Pagination_View_Datagrid($pager);
echo $view->render();
Output (obviously it would be in columns in a table):
ID Title Category
11. Foo One edit
12. Bar Two edit
13. Baz Two edit
14. Faz One edit
15. Boo Two edit
First Prev 5 6 7 8 9 Next Last


(Note that there are options to control how/when Prev/Next are displayed. Note also that ORDER BY link generation and datasource sorting functionality is also build-in to the core classes. I don't know that was mentioned above.)

I hope that clarifies that this is not just a monolithic solution. It is a component that supports a number of levels and styles of solutions. It both takes the tedium out of wiring together a pagination solution, and makes it easy to provide features that you might not normally implement given time constraints. Hopefully one of these levels above supports how you like to do pagination. Also hopefully there are other levels here that you may find helpful in specific situations. So think of this as both a toolkit from which you can build custom pagination solutions, and also a set of pre-written solutions.

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Mon Jun 15, 2009 11:28 am
by sike
hi,

after skimming through the code i had a few things come to my mind:

- i would like to see a name or id property to distinguish multiple pagers per page. i usually build the get/set parameters from this id/name (eg pager1_page, pager1_size...)
- i think the sorting stuff doesn't belong to pagination (same for datagrid). it really should be in its own package
- naming of the core class interface could be more even (page vs item)
- i found some places which could behave unexpected for the user (eg get + setNumItems)

cheers
chris

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Mon Jun 15, 2009 11:45 am
by Christopher
sike wrote: - i would like to see a name or id property to distinguish multiple pagers per page. i usually build the get/set parameters from this id/name (eg pager1_page, pager1_size...)
That is a great idea. Currently you could do multiple pagers by changing the names of the parameters to something like 'page2' and 'num_items2', but it might be nice to allow a prefix/suffix to make it easier.
sike wrote:- i think the sorting stuff doesn't belong to pagination (same for datagrid). it really should be in its own package
The actual sort is done in the datasources which are wrapped in an Adapter classes. The only thing the core classes do is maintain the column name and sort direction to pass to the datasource (there is a setOrderBy() method in the datasource). It is up to the datasource on how or whether to support sorting. For example a file datasource would only have lines and no columns.
sike wrote:- naming of the core class interface could be more even (page vs item)
I think that 'page' is always used for a range of items to be displayed, and 'item' is always used for an individual row/line. I know that pagination is mostly done for database records, but file and directory pagers are also planned.
sike wrote:- i found some places which could behave unexpected for the user (eg get + setNumItems)
How could they behave in an unexpected way?

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Mon Jun 15, 2009 12:05 pm
by sike
arborint wrote:
sike wrote:- i think the sorting stuff doesn't belong to pagination (same for datagrid). it really should be in its own package
The actual sort is done in the datasources which are wrapped in an Adapter classes. The only thing the core classes do is maintain the column name and sort direction to pass to the datasource (there is a setOrderBy() method in the datasource). It is up to the datasource on how or whether to support sorting. For example a file datasource would only have lines and no columns.
for me it doesn't matter where the actual sorting takes place. it's merely the fact that i think sorting is another problem than paginating. that's why i would seperate the two. one class - one responsibility.
arborint wrote:
sike wrote:- naming of the core class interface could be more even (page vs item)
I think that 'page' is always used for a range of items to be displayed, and 'item' is always used for an individual row/line. I know that pagination is mostly done for database records, but file and directory pagers are also planned.
oh, you are right.. i misinterpreted the interface (: i was searching for smth like getPageCount and only found getNumItems and so mixed them up...
arborint wrote:
sike wrote:- i found some places which could behave unexpected for the user (eg get + setNumItems)
How could they behave in an unexpected way?
if not documented properly you could set a datasource but get the wrong numItems. I think this may work but it may lead to confusion.

another finding is the get method of A_Pagination_Request :

Code: Select all

 
        if ($this->request != null) {
            if ($this->session->get($name)) return $this->session->get($name);
        }
 

cheers
chris

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Mon Jun 15, 2009 1:09 pm
by Christopher
sike wrote:for me it doesn't matter where the actual sorting takes place. it's merely the fact that i think sorting is another problem than paginating. that's why i would seperate the two. one class - one responsibility.
This is a real point of contention. The problem is that we want pagination to be dead simple to do. That means only one or two objects to a lot of people. We get the "what is all that code" a lot. So we consider the core class to be a Value Object that keeps everything in a central object. Your position is that sorting is another problem; my position is that my pagination solutions almost always sort. So the choice is to make me and others mad by requiring another object, or annoy you that it is included? You see our choice. I fully admit it is a trade-off. But on the other hand there really are very few values associated with pagination and the sort column and direction are two of them.
sike wrote:If not documented properly you could set a datasource but get the wrong numItems. I think this may work but it may lead to confusion.
You are right. What do you think is a solution that would reduce the confusion for you?
sike wrote:another finding is the get method of A_Pagination_Request :

Code: Select all

 
        if ($this->request != null) {
            if ($this->session->get($name)) return $this->session->get($name);
        }
 
That is support for the Skeleton Framework's Request and Session classes if used with the framework. You could also wrap another frameworks classes for use as well.

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Tue Jun 16, 2009 5:57 am
by sike
arborint wrote:
sike wrote:for me it doesn't matter where the actual sorting takes place. it's merely the fact that i think sorting is another problem than paginating. that's why i would seperate the two. one class - one responsibility.
This is a real point of contention. The problem is that we want pagination to be dead simple to do. That means only one or two objects to a lot of people. We get the "what is all that code" a lot. So we consider the core class to be a Value Object that keeps everything in a central object. Your position is that sorting is another problem; my position is that my pagination solutions almost always sort. So the choice is to make me and others mad by requiring another object, or annoy you that it is included? You see our choice. I fully admit it is a trade-off. But on the other hand there really are very few values associated with pagination and the sort column and direction are two of them.
i feel your pain.. i have written a couple of frameworks till now and you have to make decisions - some like them - some not. the point i am trying to make is that the whole system starts to become bloated the more "side jobs" the pagination package has to do. what if i need multiple column sorting? or filtering? does that also belong to pagination?
arborint wrote:
sike wrote:If not documented properly you could set a datasource but get the wrong numItems. I think this may work but it may lead to confusion.
You are right. What do you think is a solution that would reduce the confusion for you?
tough question - i see why you are doing it the way it is now. the problem is that the class supports two "modes" : with and without a datasource. i think i would try to introduce a "NullDatasource" which will eliminate the whole "is there a datasource" testing code and thus will simplify the whole class.
arborint wrote:
sike wrote:another finding is the get method of A_Pagination_Request :

Code: Select all

 
        if ($this->request != null) {
            if ($this->session->get($name)) return $this->session->get($name);
        }
 
That is support for the Skeleton Framework's Request and Session classes if used with the framework. You could also wrap another frameworks classes for use as well.
i was rather talking about the code being wrong (look at the if). it might be a good idea to refactor the whole session/request/get thing to a interface and work with that rather than supporting three different styles.

cheers
chris

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Tue Jun 16, 2009 11:46 pm
by allspiritseve
sike wrote:i feel your pain.. i have written a couple of frameworks till now and you have to make decisions - some like them - some not. the point i am trying to make is that the whole system starts to become bloated the more "side jobs" the pagination package has to do. what if i need multiple column sorting? or filtering? does that also belong to pagination?
Coming from someone who said the exact same thing about sorting as you did when I first looked at Skeleton's A_Pager class, the core class's responsibilities when it comes to sorting are as pared down as they can be. Requiring a user to specify sorting means the user also has to manually persist it, which means they have to manually instantiate a url generator (A/Pagination/Helper/Url is ours) and set up the sort params. They would then also need to grab these from the request, and set the values on their datasource. Not to say that's too hard to do, but we wanted to make it easy for the most common use case, which is sorting database records by one field, ascending or descending. If you happen to use the core and write your own view code, there's only three methods you won't use on the core. For us, that was a worthy tradeoff in order to simplify our standalone use case.
sike wrote:tough question - i see why you are doing it the way it is now. the problem is that the class supports two "modes" : with and without a datasource. i think i would try to introduce a "NullDatasource" which will eliminate the whole "is there a datasource" testing code and thus will simplify the whole class.
A datasource is a required dependency (actually, the only required dependency). I don't know where you're getting the 'two modes'. We are currently type hinting the first parameter in the core to our datasource interface (take that, Jenk :twisted:) and assuming that a datasource has been set throughout the class. Can you show me where you are seeing the 'is there a datasource testing code'?
sike wrote:i was rather talking about the code being wrong (look at the if).
How is the code wrong? Edit: Oh, I see a typo: meant $this->session->get() not $this->request->get(). Fixed in repository.
sike wrote:it might be a good idea to refactor the whole session/request/get thing to a interface and work with that rather than supporting three different styles
Well, we could require a request class but that would be another dependency and/or another class that we include in an already "start[ing] to become bloated" (your words, not mine) library as it is. I was also intending the request and session objects to potentially be used together, hence not restricting a user to passing in one or the other. For instance, you may want to persist the state of a page in the session (moving from a list to a detail page and back, for example) but have the request take over when back on that page (so you get the freshest information). That's why if you see in A_Pagination_Request it checks the $request object, then the $session object, then finally the $_GET object.
sike wrote:i would like to see a name or id property to distinguish multiple pagers per page. i usually build the get/set parameters from this id/name (eg pager1_page, pager1_size...)
I added a method setParamNamespace(). If set it will append itself to every parameter used by the system. It does not add a separating underscore, as url space is precious and some users may prefer an extra short namespace, but that would be recommended for clarity, ie:

Code: Select all

$pager = new A_Pagination_Request($datasource);
$pager->setParamNamespace('pager1_');

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Wed Jun 17, 2009 3:04 am
by Christopher
Great input Sike. .I am interested in what you are thinking about with your NullDatasource?

Hopefully you can see, whether you agree with our decisions or not, that we did put a lot of thought into the design (the discussion was a 28 page 400+ post thread ;)). And certainly there is more to do in improving the current design, plus adding Ajax support, implementing the Datagrid, etc. So your input and contributions are really appreciated.

Re: Skeleton Framework: Pagination Component - Please Review

Posted: Wed Jun 17, 2009 8:50 am
by allspiritseve
sike wrote:i think i would try to introduce a "NullDatasource"
I have been confused about what you meant by this, but I wonder if you are thinking of a use case where the core class is only used for calculations, and doesn't get its data from a datasource? I don't think we intended it to be used that way (setNumItems is used when caching the number of items in a datasource so you don't have to make repeated calls every request-- but the initial request does call getNumItems() on the datasource and getItems() is still used. Using it without a datasource is an interesting idea though. Maybe something like this?

Code: Select all

$numItems = 100;
$itemsPerPage = 10;
$pager = new A_Pagination_Request(new A_Pagination_Adapter_Null($numItems), $itemsPerPage);
Though unless you have a good reason for retrieving data manually, I'd still prefer implementing a datasource adapter.