Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zend_Paginator blurring MVC lines

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?

like image 870
Sean McSomething Avatar asked Dec 05 '08 02:12

Sean McSomething


3 Answers

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 );
    //...
}
like image 89
dcousineau Avatar answered Oct 26 '22 22:10

dcousineau


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;
    }
}
like image 42
Troy Avatar answered Oct 27 '22 00:10

Troy


I really needed a solution in which I could use a Zend_Db_Table class method as the resource for my paginator adapter, instead of an array or a Zend_Db_Select object.

This sort of advanced modeling is not compatible with the standard adapters for Zend_Paginator. I went ahead and fixed this for everyone who is desperate for an answer, as I was.

<?php

    /* /zend/Paginator/Adapter/DbTableMethod.php */    
    class Zend_Paginator_Adapter_DbTableMethod implements Zend_Paginator_Adapter_Interface {

        protected $_class;
        protected $_method;
        protected $_parameters;
        protected $_rowCount = null;

        public function __construct($class, $method, array $parameters = array()){

        $reflectionClass = new ReflectionClass($class);
        $reflectionMethod = $reflectionClass->getMethod($method);
        $reflectionParameters = $reflectionMethod->getParameters();

        $_parameters = array();

        foreach ($reflectionParameters as $reflectionParameter){

            $_parameters[$reflectionParameter->name] = ($reflectionParameter->isDefaultValueAvailable()) ? $reflectionParameter->getDefaultValue() : null;

        }       

        foreach ($parameters as $parameterName => $parameterValue){

            if (array_key_exists($parameterName, $_parameters)) $_parameters[$parameterName] = $parameterValue;

        }

        $this->_class = $class;
        $this->_method = $method;
        $this->_parameters = $_parameters;

        }

        public function count(){

            if (is_null($this->_rowCount)){

                $parameters = $this->_parameters;
                $parameters['count'] = true;

                $this->_rowCount = call_user_func_array(array($this->_class, $this->_method), $parameters);

            }       

            return $this->_rowCount;

        }

        public function getItems($offset, $itemCountPerPage){

            $parameters = $this->_parameters;
            $parameters['limit'] = $itemCountPerPage;
            $parameters['offset'] = $offset;

            $items = call_user_func_array(array($this->_class, $this->_method), $parameters);

            return $items;
        }

    }

?>

This is how it works in your controller:

    <?php

    class StoreController extends Zend_Controller_Action {

        public function storeCustomersAction(){

            $model = new Default_Model_Store();
            $method = 'getStoreCustomers';
            $parameters = array('storeId' => 1);

            $paginator = new Zend_Paginator(new Site_Paginator_Adapter_DbTableMethod($model, $method, $parameters));
            $paginator->setCurrentPageNumber($this->_request->getParam('page', 1));
            $paginator->setItemCountPerPage(20);

            $this->view->paginator = $paginator;

        }

    }

?>

The only requirements for this adapter to work is to list the following parameters in your model methods arguments list (in any order [the adapter will detect the method signature through reflection):

$limit = 0, $offset = 0, $count = false

The paginator will call your method with the appropriate values for the $limit, $offset, and $count arguments. That's it!

Example:

        <?php

        class Default_Model_Store extends Zend_Db_Table {

        public function getStoreCustomers($storeId, $includeCustomerOrders = false, $limit = 0, $offset = 0, $count = false){

if ($count) /* return SELECT COUNT(*) ... */  

                /* ... run primary query, get result */
               $select = $this->_db->select(...)->limit($limit, $offset);


               $rows = $this->_db->fetchAll($select);

               if ($includeCustomerOrders){

                  foreach ($rows as &$row){

                      $customerId = $row['id'];
                      $row['orders'] = $this->getCustomerOrders($customerId);

                   }

               }

               return $rows;    

            }

        }

    ?>
like image 45
axiom82 Avatar answered Oct 26 '22 22:10

axiom82