Can I pass an Object to the Paginator?

In models:

class Topics extends Model
{
    public $id;
    public $title;
    public $content;
    public $lastreply_id;

    ....

    public function getLastReplyInfo() {
        if ($this->lastreply_id) {
            $last_reply = Replys::findFirst($this->lastreply_id);
            $lr_user = Users::findFirst($last_reply->users_id);
            return array(
                "user_id" => $last_reply->users_id,
                "user_name" => $lr_user->name,
                "reply_time" => $last_reply->datetimes);
        } else {
            return null;
        }
    }
    ...

controllers:

use \Phalcon\Paginator\Adapter\Model as Paginator;
use Phalcon\Paginator\Adapter\NativeArray;

class TopicsController extends ControllerBase
{
    public function listAction($flag = null)
    {
        $flag = $this->filter->sanitize($flag, 'alnum');

        $numberPage = $this->request->getQuery("page", "int", 1);
        $problemId = $this->request->getQuery("pid");

        $topics = Topics::findAllTopicsBySql($flag, $problemId);

        $paginator = new Paginator(array(
            'data' => $topics,
            'limit' => 12,
            'page' => $numberPage
        ));
        $this->view->page = $paginator->getPaginator();
        $this->view->form = new TopicForm();
        $this->view->parser = new Parser();
    }
}

In volt:

{% for topic in page.items %}

$last_reply_info = $topic->getLastReplyInfo();

It can work properly(in Phalcon 3.x)

But in Phalcon4.x, I've to change the data of paginator to NativeArray, and an array item can't visit the getLastReplyInfo() method!

How to visited the getLastReplyInfo() in volt?

I don't see where you've changed to NativeArray?

Are you not using Model because of the large number of records? How many do you have?

You may want to consider using the QueryBuilder adapter, as that will let you paginate Model records.



29.6k
edited 7d ago

I don't see where you've changed to NativeArray?

Are you not using Model because of the large number of records? How many do you have?

use \Phalcon\Paginator\Adapter\Model as Paginator;
use Phalcon\Paginator\Adapter\NativeArray;

class TopicsController extends ControllerBase
{
    public function listAction($flag = null)
    {
        $flag = $this->filter->sanitize($flag, 'alnum');

        $numberPage = $this->request->getQuery("page", "int", 1);
        $problemId = $this->request->getQuery("pid");

        $topics = Topics::findAllTopicsBySql($flag, $problemId);

        $paginator = new Paginator(array(
            'data' => $topics,       <------------------------------------- $topics is an object
            'limit' => 12,
            'page' => $numberPage
        ));
        $this->view->page = $paginator->getPaginator();
        $this->view->form = new TopicForm();
        $this->view->parser = new Parser();
    }
}

As I marked above, I want to use Model, and I want to pass an object to the paginator

But in the volt, I dump out the object, it says NULL

{% for topic in page.items %}

$last_reply_info = $topic->getLastReplyInfo();   <----------- I can't use the method getLastReplyInfo(), because $topic is an array.

var_dump($topic->id)  <----------------- result is "NULL", That is because $topic is an array not an object

{% endfor %}

Switch data for model. In v4, there are different arguments depending on whether you're using the Model, NativeArray, or QueryBuilder adapters.

https://docs.phalcon.io/4.0/en/pagination#methods



29.6k
edited 6d ago

Switch data for model. In v4, there are different arguments depending on whether you're using the Model, NativeArray, or QueryBuilder adapters.

https://docs.phalcon.io/4.0/en/pagination#methods

I‘ve changed 'data' => $topics, to 'model' => $topics,, But it says:

Trying to call method toarray on a non-object
#0 /.../app/controllers/TopicsController.php(65): Phalcon\Paginator\Adapter\Model->paginate()

and it has a warning:

FastCGI sent in stderr: "PHP message: PHP Warning:  call_user_func() expects parameter 1 to be a valid callback, class 'Phalcon\Mvc\Model\Resultset\Simple' does not have a method 'find' in ....


7.8k

its been discussed here https://forum.phalcon.io/discussion/20975/phalcon4-how-to-do-model-pagination

Phalcon\Paginator\Adapter\Model expects a class / object in model option but it has to be an instance of Phalcon\Mvc\ModelInterface

if you check the source code you will see that it will call count() with the parameters option and then call find() with the parameters option and then converts the results set to an array

check the other adapters you may find another way around this

I‘ve changed 'data' => $topics, to 'model' => $topics,

Can you post your updated code?



29.6k
edited 5d ago

Can you post your updated code?

In the controller

use Phalcon\Paginator\Adapter\Model as Paginator;
use Phalcon\Paginator\Adapter\NativeArray;

class TopicsController extends ControllerBase
{
    public function initialize()
    {
        parent::initialize(); // TODO: Change the autogenerated stub
    }

    public function indexAction()
    {
        $this->dispatcher->forward(array(
            'action' => 'list'
        ));
    }

    public function listAction($flag = null)
    {
        $flag = $this->filter->sanitize($flag, 'alnum');

        $numberPage = $this->request->getQuery("page", "int", 1);
        $problemId = $this->request->getQuery("pid");

        $topics = Topics::findAllTopicsBySql($flag, $problemId);   <------------------- $topics

        $paginator = new Paginator(array(
            'model' => $topics,    <-------------------- model
            'limit' => 12,
            'page' => $numberPage
        ));

        $this->view->page = $paginator->paginate();    <------------------ this is line 65
    }
    ....

In the Model

class Topics extends Model
{
    const FRONT_PER_PAGE = 30; 

    public $id;

    public $flag;

    public $problems_id;

    public $users_id;

    public $title;

    public $content;

    public $reply_count;

    public $view_count;

    public $favorite_count;

    public $vote_count;

    public $orders;

    public $lastreply_id;

    public function getLastReplyInfo() {
        if ($this->lastreply_id) {
            $last_reply = Replys::findFirst($this->lastreply_id);
            $lr_user = Users::findFirst($last_reply->users_id);
            return array(
                "user_id" => $last_reply->users_id,
                "user_name" => $lr_user->name,
                "reply_time" => $last_reply->datetimes);
        } else {
            return null;
        }
    }

    public static function findAllTopicsBySql($flag = null, $pid = null, $limit = 0)
    {
        $sql = "SELECT
                    t.*,
                    u.name,
                    u.avatar,
                    p.title as problems_title
                FROM
                    `topics` t
                INNER JOIN users u ON u.id = t.users_id
                LEFT JOIN problems p ON p.id = t.problems_id
                WHERE t.id IS NOT NULL ";
        if ($flag) {
            $sql .= " AND `t`.`flag` = '$flag' ";
        }
        if ($pid) {
            $sql .= " AND `t`.`problems_id` = '$pid' ";
        }
        $sql .= " ORDER BY t.orders DESC, t.datetimes DESC ";
        if ($limit > 0)
            $sql .= " LIMIT $limit";

        $topics = new Topics();

        return new Resultset(null, $topics, $topics->getReadConnection()->query($sql));
    }

    ....

and the warrning & error log

FastCGI sent in stderr: "PHP message: PHP Warning:  call_user_func() expects parameter 1 to be a valid callback, class 'Phalcon\Mvc\Model\Resultset\Simple' does not have a method 'find' in /.../app/controllers/TopicsController.php on line 65" while reading response header from upstream

Trying to call method toarray on a non-object
#0 /.../app/controllers/TopicsController.php(65): Phalcon\Paginator\Adapter\Model->paginate()

Hmmm. Like @talal424 mentioned, model is only if you're wanting to query a particular model. I was wrong before to suggest using model. But I was right to suggest using QueryBuilder. That allows you to create a complex query like you have, while still using the paginator.



29.6k
edited 4d ago

Hmmm. Like @talal424 mentioned, model is only if you're wanting to query a particular model. I was wrong before to suggest using model. But I was right to suggest using QueryBuilder. That allows you to create a complex query like you have, while still using the paginator.

Thanks!

Does that mean I have to use QueryBuilder to transfer an object to the VOLT? I can't use the original method(for example findAllTopicsBySql() that's coding in Phalcon 3.x, there are many Controllers use this way)?

And more question, I have no idea how to builder a query like the sql statement in the method findAllTopicsBySql(), could you please help me giving an example of that?

QueryBuilder should return you an iterable object you can iterate like you would a Resultset. Your Volt template might not need to change at all. You'll need to write a new method and update your controllers to use it instead of findAllTopicsBySql().

QueryBuilder is set up to mimic the structure of queries as closely as possible. I suggest you look through the documentation - it's pretty straightforward. An example might be something like:

$Topics = $this->modelsManager->createBuilder()
            ->columns(['t.*','u.name','u.avatar','p.title'=>'problems_title'])
            ->from('topics','t')
            ->innerJoin('users','u','u.id = t.users_id')
            ->leftJoin('problems','p','p.id = t.problems_id')
            ->where('t.id IS NOT NULL')
            ->getQuery()
            ->execute();


29.6k
edited 3d ago

QueryBuilder should return you an iterable object you can iterate like you would a Resultset. Your Volt template might not need to change at all. You'll need to write a new method and update your controllers to use it instead of findAllTopicsBySql().

QueryBuilder is set up to mimic the structure of queries as closely as possible. I suggest you look through the documentation - it's pretty straightforward. An example might be something like:

Thanks a lot !

I've formed the QueryBuilder like below, and it maybe can work:

$builder = $this
            ->modelsManager
            ->createBuilder()
            ->columns(['t.*','u.name','u.avatar','problems_title'=>'p.title'])
            ->from(['t' => Topics::class])
            ->innerJoin(Users::class,'u.id = t.users_id', 'u')
            ->leftJoin(Problems::class,'p.id = t.problems_id', 'p')
            ->where('t.id IS NOT NULL');

        $paginator = new QueryBuilder(
            [
                "builder" => $builder,
                "limit"   => 12,
                'page' => $numberPage
            ]
        );

        $this->view->page = $paginator->paginate();

In the VOLT page, I output the result like this {% for topic in page.getItems() %} , and I got a strange problem: I can visit $topic->avatar , $topic->name and $topic->problems_title, but I CAN'T visit $topic->users_id or other fields of topics.

The way $topic->avatar can output correctly, but $topic->users_id says FastCGI sent in stderr: "PHP message: PHP Notice: Undefined property: Phalcon\Mvc\Model\Row::$users_id.

I think maybe it is the problem of QueryBuilder. Below is some parts of results of the var_dump($topic):

object(PDOStatement)#106 (1) {
      ["queryString"]=>
      string(702) "SELECT `t`.`id` AS `_t_id`, `t`.`flag` AS `_t_flag`, `t`.`problems_id` AS `_t_problems_id`, `t`.`users_id` AS `_t_users_id`, `t`.`title` AS `_t_title`, `t`.`content` AS `_t_content`, `t`.`reply_count` AS `_t_reply_count`, `t`.`view_count` AS `_t_view_count`, `t`.`favorite_count` AS `_t_favorite_count`, `t`.`vote_count` AS `_t_vote_count`, `t`.`orders` AS `_t_orders`, `t`.`datetimes` AS `_t_datetimes`, `t`.`lastreply_id` AS `_t_lastreply_id`, `u`.`name` AS `name`, `u`.`avatar` AS `avatar`, `p`.`title` AS `problems_title` FROM `topics` AS `t`  INNER JOIN `users` AS `u` ON `u`.`id` = `t`.`users_id` LEFT JOIN `problems` AS `p` ON `p`.`id` = `t`.`problems_id` WHERE `t`.`id`  IS NOT NULL LIMIT :APL0"
    }
    ["result":protected]=>
    NULL
    ["rowCount":protected]=>
    int(12)
    ["sqlStatement":protected]=>
    string(702) "SELECT `t`.`id` AS `_t_id`, `t`.`flag` AS `_t_flag`, `t`.`problems_id` AS `_t_problems_id`, `t`.`users_id` AS `_t_users_id`, `t`.`title` AS `_t_title`, `t`.`content` AS `_t_content`, `t`.`reply_count` AS `_t_reply_count`, `t`.`view_count` AS `_t_view_count`, `t`.`favorite_count` AS `_t_favorite_count`, `t`.`vote_count` AS `_t_vote_count`, `t`.`orders` AS `_t_orders`, `t`.`datetimes` AS `_t_datetimes`, `t`.`lastreply_id` AS `_t_lastreply_id`, `u`.`name` AS `name`, `u`.`avatar` AS `avatar`, `p`.`title` AS `problems_title` FROM `topics` AS `t`  INNER JOIN `users` AS `u` ON `u`.`id` = `t`.`users_id` LEFT JOIN `problems` AS `p` ON `p`.`id` = `t`.`problems_id` WHERE `t`.`id`  IS NOT NULL LIMIT :APL0"
  }
  ["columnTypes":protected]=>
  array(4) {
    ["t"]=>
    array(7) {
      ["type"]=>
      string(6) "object"
      ["model"]=>
      string(18) "YZOI\Models\Topics"
      ["column"]=>
      string(1) "t"
      ["balias"]=>
      string(1) "t"
      ["instance"]=>
      object(YZOI\Models\Topics)#90 (27) {
        ["id"]=>
        NULL
        ["flag"]=>
        NULL
        ["problems_id"]=>
        NULL
        ["users_id"]=>
        NULL
        ["title"]=>
        NULL
        ["content"]=>
        NULL
        ["reply_count"]=>
        NULL
        ["view_count"]=>
        NULL
        ["favorite_count"]=>
        NULL
        ["vote_count"]=>
        NULL
        ["orders"]=>
        NULL
        ["lastreply_id"]=>
        NULL
        ["container":protected]=>
        object(Phalcon\Di\FactoryDefault)#8 (3) {
          ["services":protected]=>
          array(26) {

I think the problem may be that you're combining a * with named fields. If you were just asking for t.*, then the QueryBuilder would return a Topic instance. Since you have named columns in there as well, QueryBuilder needs to combine. You can see in the sqlStatement section that the column you're looking for gets named _t_users_id. My guess is it does that to avoid column name conflicts (like 2 tables with an id column).

I'm not certain that's a bug - that may be expected behaviour in this situation. However, I'm not certain it isn't a bug. On this page: https://docs.phalcon.io/4.0/en/db-phql#results if you scroll down almost to the end of that section it says

If you mix * selections from one model with columns from another, you will end up with both scalars as well as objects.

It doesn't say anything about renaming columns. It might be worthwhile mentioning this problem in Github.

For now I think you have 2 options:

  1. Explicitly name each column it topics that you want to reference.
  2. Update your Volt code to refer to the renamed fields (ie: _t_users_id


29.6k

For now I think you have 2 options:

  1. Explicitly name each column it topics that you want to reference.
  2. Update your Volt code to refer to the renamed fields (ie: _t_users_id

Thanks, I use the explicitly fields name, and it works!

But the pagination {% for topic in page.getItems() %} makes the object $topic to be the instance of Phalcon\Mvc\Model\Row, I want to keep it to be the instance of its original Topics::class, because I want to call the member function of Topics::class. How to?