Hao Zhou's avatar

Undefined function appends() in custom Laravel 5 paginator

Hi guys,

I am using paginator to show data in a tabular format.

There is a small requirement when it comes to where to place those links of pages: I want to float them right. Considering that Laravel is using Bootstrap 3 to render the layout, I know I can simply add class "pull-right" into the element.

So I constructed a custom pagination presenter like so:

    namespace App\Http\Presenters;

    use Illuminate\Pagination\BootstrapThreePresenter;

    class DatatablePaginationPresenter extends BootstrapThreePresenter{

        public function render()
        {
            if ($this->hasPages())
            {
                return sprintf(
                '<ul class="pagination pull-right">%s %s %s</ul>',
                $this->getPreviousButton(),
                $this->getLinks(),
                $this->getNextButton()
               );
            }
            return '';
        }
    } 

And the code in the template file:

    <div class="row">
        <div class="col-md-4">Some text</div>
        <div class="col-md-8">
        {!!  with(new App\Http\Presenters\DatatablePaginationPresenter($articles))->render() !!}
        </div>
    </div>

Laravel works without errors until I add some parameters in those links by calling appends(), for example:

    {!!  with(new App\Http\Presenters\DatatablePaginationPresenter($articles))->appends(['sort' => $column,'order' => $order,'term' => $term])->render() !!}

This time I got a FatalException saying Call to undefined method App\Http\Presenters\DatatablePaginationPresenter::appends()

I walked through some source code to find out how appends() works. It is declared in the \Illuminate\Contracts\Pagination\Paginator interface, and any class implements this interface should define it. Given that my Article class extends Eloquent, getting a paginated collection should get a paginator which already implements appends(). So it is really weird that appends() is not defined.

Here is the code of my repo/service layer returning paginated data to my controller.

    $articles =  Article::with('category')
            ->select($columns)
            ->orderBy($column,$order)
            ->paginate($itemPerPage);

    return $articles;
0 likes
10 replies
usman's avatar

@Hao Zhou Inside your controller do the appends on the paginator instance you are getting from the repo, then inside the view, render using the custom presenter:

<?php $presenter = new App\Http\Presenters\DatatablePaginationPresenter() ?>
{{$articles->render($presenter)}}

It would be better if you pass the custom presenter form the controller action, or use a composer.

Update://

You can also use your own code, just call the appends on the articles instance and then pass it to your custom presenter. The problem here is that the base BootstrapThreePresenter does not provide a decorator method for appends.

Usman.

Hao Zhou's avatar

@usman

Quickly tested your solution and I am yet to get what I expect.

As you suggested, I modified the controller:

$presenter = new App\Http\Presenters\DatatablePaginationPresenter($articles);

return view('admin/articles/index')
            ->with('articles',$articles)
            ->with('otherStuff',$otherStuff)
            ->with('presenter',$presenter);

In the view:

{!!  $articles->appends(['sort' => $column,'order' => $order,'term' => $term])->render($presenter) !!}

(Honestly such usage is kinda awkward. I mean constructing object/calling method by passing $articles/$presenter as argument to each other :) )

No exception was thrown, but those links to page are incorrect though: For example, now link to page 2 is admin/articles?page=2 instead of admin/articles?sort=id&order=desc&page=2

Any idea?

Hao

usman's avatar

@Hao Zhou :

$articles->appends(['sort' => $column,'order' => $order,'term' => $term]);
$presenter = new App\Http\Presenters\DatatablePaginationPresenter($articles);

return view('admin/articles/index')
            ->with('articles',$articles)
            ->with('otherStuff',$otherStuff)
            ->with('presenter', $presenter);

//and then inside your view use:
{{$articles->render($presenter)}} 

Hope this will solve the issue.

//Updated my reply.

Usman.

usman's avatar

Ok, here is another way, inside your DatatablePaginationPresenter class define the appends method and use the original code that you were already using:

    namespace App\Http\Presenters;

    use Illuminate\Pagination\BootstrapThreePresenter;

    class DatatablePaginationPresenter extends BootstrapThreePresenter{
    
        public function appends(array $appends) 
        {
           $this->paginator->appends($appends);
           return $this;
        }
        public function render()
        {
            if ($this->hasPages())
            {
                return sprintf(
                '<ul class="pagination pull-right">%s %s %s</ul>',
                $this->getPreviousButton(),
                $this->getLinks(),
                $this->getNextButton()
               );
            }
            return '';
        }
    } 

Usman.

Hao Zhou's avatar

@usman

The latter approach is what I was thinking too.

I undid all your proposed changes, and added appends() in the custom presenter.

But those links were still something like admin/articles?page=2 rather than admin/articles/sort=id&order=desc&page=2

Code in presenter:

<?php
namespace App\Http\Presenters;

use Illuminate\Pagination\BootstrapThreePresenter;

class DatatablePaginationPresenter extends BootstrapThreePresenter{

    public function appends(array $parameters)
    {
        $this->paginator->appends($parameters);
        return $this;
    }

    public function render()
    {
        if ($this->hasPages())
        {
            return sprintf(
                '<ul class="pagination pull-right">%s %s %s</ul>',
                $this->getPreviousButton(),
                $this->getLinks(),
                $this->getNextButton()
            );
        }
        return '';
    }
}

And in the view:

{!!  with(new App\Http\Presenters\DatatablePaginationPresenter($articles))->appends(['sort' => $column,'order' => $order,'term' => $term])->render() !!}
usman's avatar

@Hao Zhou are you sure the variable $column, $order and $term are not empty. Try dumping them, I think they are null, where are they coming from? I don't see you passing them to the view from the action.

Usman.

Hao Zhou's avatar

@usman

I placed the custom paginator/presenter and the default one on the top of and at the bottom of the table, respectively. I even hard coded the parameters for the custom one:

{!! with(new App\Http\Presenters\DatatablePaginationPresenter($articles))->appends(['sort' => 'id','order' => 'asc','limit' => '10','term' => 'hello'])->render() !!}
{!!  $articles->appends(['sort' => $column,'order' => $order,'limit' => $limit,'term' => $term])->render() !!}

It is very interesting to observe that even I hard coded, the urls generated by custom presenter were still like admin/articles?page=2, admin/articles?page=3 and so on.

appends() did not work in custom presenter.

Back to your question. Pretty sure they have values once user starts to sort by a column or to search. I used the default paginator before by just calling appends() on $articles. It worked well.

If you are curious about where these variables come from, here is the code in controller (for simplicity, let's just set $column):

    // Sort by which column?
        $column= $request->input('sort');
    // and after all the processing, return $column to the view in order to construct links, like pagination
    return view('admin/articles/index')->with('column',$column);

$column will be passed to repo/service layer to be used in orderBy(). There is a check:

        $column= is_null($column) ? 'id' : $column;

So except for the first time when users visit the article table, once they start to sort by a column, input sort always has a value. More code in the view, anchors that let users sort by a column:

<th>{{trans('ui.articles.id')}} <a data-toggle="tooltip" data-placement="top" title="{{trans('ui.dataTable.sort')}}" href='{{url("admin/articles?sort=id")}}'></a></th>
<th>{{trans('ui.articles.title')}} <a data-toggle="tooltip" data-placement="top" title="{{trans('ui.dataTable.sort')}}" href='{{url("admin/articles?sort=title")}}'></a></th>

You can see different anchors set a value to 'sort' differently.

usman's avatar
usman
Best Answer
Level 27

@Hao Zhou It is pretty strange, I am unable to reproduce this bug with the custom presenter, remove the appends method from the custom presenter and just go with:

{!! with(new App\Http\Presenters\DatatablePaginationPresenter($articles->appends(['sort' => $column,'order' => $order,'limit' => $limit,'term' => $term])))->render() !!}

I hope :) you will get it right this time.

Usman.

1 like
Hao Zhou's avatar

@usman

I checked the Laravel source again and found out what's going wrong.

It was my mistake that thinking BootstrapThreePresenter implements the Paginator Interface, but it does not. So it is wrong to call appends() on Presenter object.

{!! with(new MyPresenter($paginatorObject))->appends(array())->render() !!}

And you are right. I have to call appends() on a Paginator instance first, in my case the $articles, then pass it to custom presenter constructor.

Thanks heaps!

Please or to participate in this conversation.