views:

181

answers:

4

I use rich domain model in my app. The basic ideas were taken there. For example I have User and Comment entities. They are defined as following:

<?php
class Model_User extends Model_Abstract {

    public function getComments() {
        /**
        * @var Model_Mapper_Db_Comment
        */
        $mapper = $this->getMapper();
        $commentsBlob = $mapper->getUserComments($this->getId());
        return new Model_Collection_Comments($commentsBlob);
    }

}

class Model_Mapper_Db_Comment extends Model_Mapper_Db_Abstract {

    const TABLE_NAME = 'comments';

    protected $_mapperTableName = self::TABLE_NAME;

    public function getUserComments($user_id) {
        $commentsBlob = $this->_getTable()->fetchAllByUserId((int)$user_id);
        return $commentsBlob->toArray();
    }
}

class Model_Comment extends Model_Abstract {

}
?>

Mapper's getUserComments function simply returns something like:

return $this->getTable->fetchAllByUserId($user_id)

which is array. fetchAllByUserId accepts $count and $offset params, but I don't know to pass them from my Controller to this function through model without rewriting all the model code.

So the question is how can I organize pagination through model data (getComments). Is there a "beatiful" method to get comments from 5 to 10, not all, as getComments returns by default.

+1  A: 

Zend_Paginator may be the simple solution you are looking for. It can take any array() or instance of an Iterator (which a Zend_Db_Table_Rowset is)

$paginator = Zend_Paginator::factory($model->getComments());
$paginator->setItemCountPerPage(5);
$paginator->setCurrentPageNumber($this->getRequest()->getParam('page',1));
$this->view->comments = $paginator;

In the view:

<?php foreach($this->comments as $comment): ?>
   Render your HTML for the comment
<?php endforeach; ?>
<?php echo $this->paginationControl($this->comments, 'Sliding', '_pagination.phtml'); ?>

And a (very) simple paginationControl() partial (taken from this blog post):

<?php if ($this->pageCount): ?>
<div class="paginationControl">
  <?php if (isset($this->previous)): ?>
    <a href="<?= $this->url(array(’page’ => $this->previous)); ?>">&lt; Previous</a> |
  <?php else: ?>
    <span class="disabled">&lt; Previous</span> |
  <?php endif; ?>

  <?php foreach ($this->pagesInRange as $page): ?>
    <?php if ($page != $this->current): ?>
      <a href="<?= $this->url(array(’page’ => $page)); ?>"><?= $page; ?></a> |
    <?php else: ?>
      <?= $page; ?> |
    <?php endif; ?>
  <?php endforeach; ?>

  <?php if (isset($this->next)): ?>
    <a href="<?= $this->url(array(’page’ => $this->next)); ?>">Next &gt;</a>
  <?php else: ?>
    <span class="disabled">Next &gt;</span>
  <?php endif; ?>
</div>
<?php endif; ?>

More examples of Zend_Paginator are available via a google search.

gnarf
No. This means that I should to get ALL the comments and then paginate through them as array. This is not the appropriate solution.
Ololo
If you are worried about querying too much from the SQL - `Zend_Paginate` can take a `Zend_Db_Select` as well and it will automatically add the proper `LIMIT` clauses. You can also write custom adapters for `Zend_Paginate` that can handle any arbitrary class. The interface is quite powerful actually.
gnarf
I know about Zend_Paginate adapters. And in common case this is just an instrument to make pagination. But I'm talking about how to perform getting a section of data without returning SQL statements from the model to the controller, which is not good. If I could create some transparent mechanism to get partial data from getComments, then I will create my own Zend_Paginate adapter.
Ololo
I guess I'm just not understanding why you feel returning all the comments then filtering them with a Paginator in the controller is bad...
gnarf
Because comments is just an example. There could be a thousands of records, so returning all is not good idea. Imagine that we are creating paginator for all Stackoverflow posts. I want to find a common solution to use it as a best-practice. Anyway thank you for your help.
Ololo
@user246790 - In those cases I don't really see problems with making the model implement the `Zend_Paginator_Adapter_Interface` being a bad solution, will update with a proposal
gnarf
gnarf is right. Z_P is great. Since you're doing rich domain stuff and don't want Zend_Db_Table anywhere near your controllers (which is a good thing), your only choice is to implement pagination in your models (in your Gateway and Mapper layers). Otherwise you're just demanding that magic happens.
timdev
Ok, thank to you all. I will try to use Z_P in some way.
Ololo
+1  A: 

If you only care about paginating the results the user sees, and aren't concerned about improving your performance, you can probably avoid building pagination into your model infrastructure.

Assuming you've got some kind of Model_Collection_Abstract class which all your collection classes descend from, you could likely hack pagination into that.

So then you'd have code that looks something like:

<?PHP
    //$comments is a subclass of Model_Collection_Abstract, which implements the paging stuff
    $comments = $user->getComments(); 
    $comments->setStart(10);
    $comments->setPageLength(10);

    $numPages = $comments->numPages(); //can be derived from the pagelength and the collection's internal record store.

   $currentPage = $comments->currentPage(); //can be derived from start and page length

    foreach($comments as $comment){
       //this code runs up to ten times, starting at the tenth element in the collection.
    }

The downside here is that you're always grabbing all the comments, even if you only want to see ten of them. But this might be an acceptable solution for you.

If you only want N records pulled from the database (for N=number to display), then of course you'll need to implement some way to pass start/limit, or equivalent, parameters, all the way down through your model.

EDIT: The other answer about looking at Zend_Paginator is worth reading too. If your collection class implements Iterator, it's quite possible that you can plug it into Zend_Paginator and save some serious headaches. I've not done this (yet), but it's worth having a look!

timdev
+1  A: 

Thinking about this too right now. Have you found a neat solution yet? My thoughts so far are 1) avoid using anything like Zend_Paginator and just do everything on your own. i.e you get params from the environment (request object or some config file etc) like itemCountPerPage, currentPageNumbe, you pass those into your service layer method (or in your case it's just mapper) like

$comments = $this->getCommentsMapper()->getPage($itemCountPerPage, $currentPage);

then you request total item amount from your mapper (it's fully up to you if you should do this in a separate request or not) like

$totalComments = $this->getCommentsMapper()->getTotal();

So now you have all data to make a "pagination control view". Speaking "Zendy" you pass all these variables to the view and draw the pagination control by yourself.

2) Develop an adapter for Zend_Paginator that would LIE about its state. I.e. instead of having all items it will just have some part of them that you need, but you will manually set "total count" value and others. I don't really like this approach myself.

3) Add some special domain object called "Page" that would be returned from mappers or service layers. Such object would encapsulate all those variables we need to build a pagination control presentation. With this we would be able to do the following:

` $commentsService = $this->getCommentsService(); $commentsService->setItemCountPerPage(10); // let say 10 is taken from some config.ini

$commentsPage = $this->getCommentsService()->getPage(12); $this->view->commentsPage = $commentsPage;

`

Somewhere in the view we can get these ` $commentsPage->getItems(); // collection of comments

$commentsPage->getCurrentPageNumber();

$commentsPage->getTotalItemsCount();

`

which is enough to build a pagination control.

This it all.

I'm currently choosing between first and third approaches, probably will go with third.

lcf
I have found the compromise solution. I think the best is to return Paginator object from the mapper. Why? Because mappers are working with concrete storage type (i.e. database), so there is nothing bad if DB mapper will create raw Zend_Paginate object.
Ololo
Here is the link to my solution: http://stackoverflow.com/questions/2883900/pagination-in-a-rich-domain-model/3091917#3091917
Ololo
A: 

Here is my solution:

class Model_Mapper_Db_Comment extends Model_Mapper_Db_Abstract {

    public function getUserCommentsPaginator($user_id) {
        $select = $this->_getTable()->select()->where('user_id = ?', (int)$user_id);
        $paginator = Zend_Paginator::factory($select, 'DbSelect');
        return $paginator;
    }
} 

class Model_User extends Model_Abstract implements Zend_Auth_Adapter_Interface {

    public function getCommentsPaginator() {
        $paginator = $this->getMapper(null, 'Comment')->getUserCommentsPaginator($this->id);
        $paginator->setFilter(new App_Filter_Array_Collection('Model_Collection_Comments'));
        return $paginator;
    }
}

Model requests Zend_Paginate object with prepared query based on user_id provided from Model. Then Model added filter to Zend_Paginate object to make it compatible with other Model methods (return Model_Collection classes, not arrays). All other pagination parameters are set in controller (page, the number of items per page, etc.).

Thet's how I had separated storage, business and control logic in my application.

Ololo