We are moving our forum in GitHub Discussions. For questions about Phalcon v3/v4 you can visit here and for Phalcon v5 here.

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.



30.9k
edited Nov '20

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



30.9k
edited Nov '20

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 ....


8.4k

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?



30.9k
edited Nov '20

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.



30.9k
edited Nov '20

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();


30.9k
edited Nov '20

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


30.9k

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?

You can't. Since you're mixing named columns (or even when you were mixing * and named columns) from different tables, the result returned can't be an instance of a model - it's just an object.

If you absolutely need to execute Topic functions, then I think you're going to need to fall back on the ORM. Using model relationships, you can just do a Topics::find(), then in your Volt code, reference $topic->user->name, $topic->user->avatar, and $topic->problem->title. This will add 2 more queries per topic, but since your page size is only 12, not too bad.



8.4k

when selecting Model.* there should be a property in the result object and the value would be an instance of that model other direct selected columns like u.name there should be a property in the result object and the value should would be the coulmn's value

$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');

$results = $builder->getQuery()->execute(); // Phalcon\Mvc\Model\Resultset\Complex

$firstRow = $results->getFirst(); // Phalcon\Mvc\Model\Row 

$firstRow->t; // this should be an instance of Topics

$firstRow->name; // this should be the value of the selected column