views:

1024

answers:

6

I'm working on a Zend Framework (1.7) project with a structure loosely based on the structure of the quickstart application - front controller, action controllers, views & models that use Zend_Db_Table to get to the database. One of my main models relies on some expensive joins to pull up its primary listing, so I'm looking into using Zend_Paginator to reduce the number of rows brought back from the database. My problem is that Zend_Paginator only comes with 4 adaptors, none of which really seem to be a good fit for me.

  • Array : Building the array to feed to ZP would involve fetching all the records which is what I'm trying to avoid
  • Iterator : A dumb iterator would present the same problems as an array and a smart one feels like it would be a poor fit for the Model
  • DbSelect : Getting a DbSelect object up to the Controller would uncomfortably tie the Controller to the inner workings of my DB (not to mention producing raw result rows rather than encapsulated objects)
  • DbTableSelect : same as DbSelect
  • Null Adapter : pass all the details back and forth manually.

Passing the paginator into the model feels like it, too, would violate the MVC separation. Is the problem that I've built my model incorrectly, that I'm being to dogmatic about maintaining MVC separation or am I missing a clean, elegant way of sticking all the moving parts together?

A: 

Well, I can't give you an answer to your concerns with using DbSelect but I did come across this bit of code (in the comments of the ibuildings blog) relating to the issue of reducing the number of rows pulled. Might be useful to some readers.

$select = $db->from('users')->order('name');    
$paginator = new Zend_Paginator(new Zend_Paginator_Adapter_DbSelect($select));
$paginator->setCurrentPageNumber($this->_getParam('page', 1));
$paginator->setItemCountPerPage(25);
$this->view->paginator = $paginator;
gaoshan88
+1  A: 

You can provide an interface on your models that accepts $current_page and $per_page parameters and returns the current page's data set as well as a paginator object.

This way all your pagination code is contained within the model and you are free to use the Db adapters without feeling like you've broken the concept.

Plus, the controller really shouldn't be setting up the pager anyways since you are correct in it being tied to the data (and models are for data, not just database connections).

class Model
{
    //...
    /**
     * @return array Array in the form: array( 'paginator' => obj, 'resultset' => obj )
     */
    public function getAll( $where = array(), $current_page = null, $per_page = null );
    //...
}
dcousineau
A: 

An important consideration to take into account when working with MVC is that the model is for all domain logic, whereas the controller is for the business logic. A general rule of thumb is that the model(s) should have no knowledge of the interface (controllers or views), but they need not be simple DB accessors. To be as portable as possible, they should know nothing of formatting or display properties either (unless that is part of the domain logic).

In fact, all logic that manipulates domain logic should be in the model and not in the controllers. The controller should pass information from the interface, transforming it as needed, to the model, and select which view to display/update. If it doesn't have anything to do with the interface, it might better be represented in a model than in a controller, as it would allow for more code reuse if you decide to swap out the controller/view pairings later.

Ideally, your model should provide an interface for accessing the information you need. How that is implemented behind that interface is not a concern of MVC, so long as the model remains unaware of the VC portion of MVC. If that means passing around a paginator object, that is not a direct violation of the MVC principles, though if the paginator has anything to do with rendering itself (sorry, I don't know Zend), it might be best to pass an interface of it in (that is missing the rendering methods), have the model manipulate/fill it in, then pass it back out. That way you don't have rendering code being generated from the model, and you could replace the paginator implementation if you decided to make your application a console app later (or add an API interface of some sort).

Sydius
A: 

If you use the DbSelect adapter you can simply pass in the resultset and this goes a long way in maintaining some separation. So in your controller:

$items = new Items();//setup model as usual in controller
$this->view->paginator = Zend_Paginator::factory($items->getAll()); //initialize the pagination in the view NB getAll is just a custom function to encapsulate my query in the model that returns a Zend_Db_Table_Rowset
$this->view->paginator->setCurrentPageNumber($page); //$page is just the page number that could be passed in as a param in the request
$this->view->paginator->setView($this->view);

In the view you can access the data through the paginator

<?php foreach($this->paginator as $item):?>
 <?=$item->someProperty?>
<?php endforeach;?>

This is a simplified example (I also setup up default scrolling style and default view partial in the bootstrap), but I thnk setting it up in the controller isn't bad because the data retrieved from the model is placed into the view by the Controller anyway and this implementation makes use of the resultset NOT the model.

Akeem
But doesn't this seem wasteful? A call to getAll() - an expensive call that may be returning thousands or millions of records - then ignore all but the 20 or so you might need for one page of displayed data?
David Weinraub
+1  A: 

There is now a setFilter method for Zend_Paginator that allows you to load the data from the row object to any model object you want:

class Model_UserDataMapper {
    public function getUsers($select, $page) {
        $pager = Zend_Paginator::factory($select);
        $pager->setItemCountPerPage(10)
                    >setCurrentPageNumber($page)
                    ->setFilter(new Zend_Filter_Callback(array($this,'getUserObjects')));
    }

    public function getUserObjects($rows) {
        $users = array();

        foreach($rows as $row) {
            $user  = new Model_User($row->toArray());

            $users[] = $user;
        }

        return $users;
    }
}
Troy
A: 

You could also implement the Zend_Paginator_Adapter_Interface directly or extend Zend_Paginator_Adapter_DbSelect in any model that needs to support pagination.

That way, the model doesn't directly know anything about View, Controller or even the Zend_Paginator, but can be directly used with the Zend_Paginator wherever it makes the most sense.

class ModelSet extends Zend_Paginator_Adapter_DbSelect
{
    public function __construct( ... )
    {
        // Create a new Zend_Db_Select ($select) representing the desired
        // data set using incoming criteria
        parent::__construct($select);
    }
    ...
}

With something like this, you can directly instantiate a pager using an instance of this class wherever it makes the most sense:

$modelSet = new ModelSet( ... );
...
$pager = new Zend_Paginator( $modelSet );
$pager->setItemCountPerPage( ... );
$pager->setCurrentPageNumber( ... );
...
// The first time the record set is actually retrieved will be at the beginning
// of the first traversal
foreach ($pager as $record)
{
    // ... do stuff with the record ...
}

Now, you can use this class as the base class for any "Model" that is a set.

Elmo