Laravel 5 - Finding the pagination page for a model
Asked Answered
S

2

8

I am working on building a basic forum (inspired by laracasts.com/discuss). When a user posts a reply to a thread:

  • I'd like to direct them to the end of the list of paginated replies with their reply's anchor (same behavior as Laracasts).
  • I'd also like to return the user to the correct page when they edit one of their replies.

How can I figure out which page a new reply will be posted on (?page=x) and how can I return to the correct page after a reply has been edited? Or, from the main post listing, which page the latest reply is on?

Here is my current ForumPost model (minus a few unrelated things) -

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * Class ForumPost
 *
 * Forum Posts table
 *
 * @package App
 */
class ForumPost extends Model {
    /**
     * Post has many Replies
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function replies()
    {
        return $this->hasMany('App\ForumReply');
    }

    /**
     * Get the latest reply for a post
     * @return null
     */
    public function latestReply()
    {
        return $this->replies()->orderBy('created_at', 'desc')->first();
    }

}

UPDATE

Take a look at this and let me know what you think. It's a bit weird in how it works but it's returning the correct page for a given reply ID and it's just one method:

public function getReplyPage($replyId = null, $paginate = 2)
    {
        $id = $replyId ? $replyId : $this->latestReply()->id;
        $count = $this->replies()->where('id', '<', $id)->count();

        $page = 1; // Starting with one page

        // Counter - when we reach the number provided in $paginate, we start a new page
        $offset = 0;

        for ($i = 0; $i < $count; $i++) {

            $offset++;
            if ($offset == $paginate) {
                $page++;
                $offset = 0;
            }
        }


        return $page;
    }
Satisfied answered 19/3, 2015 at 21:40 Comment(2)
commenting on your update: $count % $paginate will return the left over ($offset) of $count/$paginate so 11%2 = 1, that will give you the offset. The pageno can be checked by using floor($count / $paginate).. Might be a little improvement.Peristyle
Yep, thanks.. that was late night coding with a little wine involved ;) I think I'm on the right track thanks to @MirroredFate but I'm still wondering how the great Jeffrey Way accomplished this. I've since found a few other folks trying to figure this out without much luck. Mostly wondering what the most efficient way to accomplish this actually is, not sure what kind of performance hit there is running these queries for every single Post object in my listingSatisfied
A
10

Fundamentally you are working with two values: first, what the index of a reply is in relation to all the replies of a post, and second the number of replies in on a page.

For example, you might have a reply with an id of 301. However, it is the 21st reply on a specific post. You need to some way to figure out that it is the 21st reply. This is actually relatively simple: you just count how many replies are associated with that post but have smaller ids.

//get the number of replies before the one you're looking for
public function getReplyIndex($replyId)
{
    $count = $this->replies()->where('id', '<', $replyId)->count();
    return $count;
}

That method should return the index of the reply you are looking for based- assuming, of course, that your replies are using auto-increment ids.

The second piece of the puzzle is figuring out which page you need. This is done using integer division. Basically you just divide the number normally, but don't use the remainder. If you are looking at the 21st reply, and you have 10 replies to a page, you know it should be on the third page (page 1: 1-10, page 2: 11-20, page 3: 21-30). This means you need to integer divide your reply index by your replies-per-page, then add 1. This will give us 21/10+1, which, using integer division, gives us 3. Yay!

//divides where we are by the number of replies on a page and adds 1
public function getPageNumber($index, $repliesPerPage)
{
    $pageNumber = (int) ($index/$repliesPerPage+1);
    return $pageNumber;
}

Alright, so now you just need to pull that page. This simply requires a method where you specify what page number you need, and how many replies to a page there are. That method can then calculate the offset and the limit, and retrieve the records you need.

public function getPageOfReplies($pageNumber, $repliesPerPage)
{
    $pageOfReplies = $this->replies()->offset($pageNumber*$repliesPerPage)->limit($repliesPerPage)->get();
    return $pageOfReplies;
}

For good measure, though, we can build a method to get the index of the final reply.

public function getLastReplyIndex()
{
    $count = $this->replies()->count();
    return $count;
}

Great! Now we have all the building blocks we need. We can build some simple methods that use our more general-purpose ones to easily retrieve the data we need.

Let's start with a method that gets the entire page of replies on which a single reply resides (feel free to change the names (also I'm assuming there are 10 replies per page)):

public function getPageThatReplyIsOn($replyId)
{
    $repliesPerPage = 10;
    $index = $this->getReplyIndex($replyId);
    $pageNumber = $this->getPageNumber($index, $repliesPerPage);
    return $this->getPageOfReplies($pageNumber, $repliesPerPage);
}

For good measure, we can make a method that gets the page of final replies.

public function getFinalReplyPage()
{
    $repliesPerPage = 10;
    $index = $this->getLastReplyIndex();
    $pageNumber = $this->getPageNumber($index, $repliesPerPage);
    return $this->getPageOfReplies($pageNumber, $repliesPerPage);
}

You could build a variety of other methods to use our building block methods and jump around pages, get the pages after or before a reply, etc.

A couple notes

These all go in your ForumPost model, which should have a one-to-many relationship with your replies.

These are a variety of methods that are meant to provide a wide array of functionality. Don't be afraid to read through them and test them individually to see exactly what they are doing. None of them are very long, so it shouldn't be difficult to do that.

Areta answered 19/3, 2015 at 21:53 Comment(14)
Would you be so kind as to provide an example? One issue is returning to the page where a reply resides after a user edits it - for example, the reply is the first entry on page 2 of 2. If someone deletes a reply from Page 1, the reply being edited will now be on Page 1 instead of Page 2. So I will need to get the correct page immediately after saving either a new reply or an edited one.Satisfied
Thanks for the example, love it apart from the issue of figuring out which page a specific reply resides on.. any ideas? Thanks for your help, I have to head home but I will check this again in a couple of hours. :)Satisfied
Also, another critical element - I need to know the page a reply resides on from the main posts index (as seen on laracasts.com). So it's imperative that I have a way to get the exact page of a reply on the fly for create, update, and index.Satisfied
Wow, that's a lot of functions! I've read your answer a few times and still trying to piece it all together. One method, getReplyOffset(), is missing or perhaps under the wrong name - can't find anything that really matches..Satisfied
If you could test some of this when you get a chance I'd really appreciate it.. there's a lot going on and I'm not sure what goes where, which method names are incorrect or missing. The best I've been able to do in trying to find which page a reply is on is to return that reply's model - not sure how I got that.Satisfied
Thanks for breaking things down in more detail. I guess what confused me were methods that returned reply objects, not really what I was after - I just needed the integer of the page where a reply resides. I will play around with this in a while, thanks so much for all of your hard work on this!Satisfied
@Satisfied You could just combine the methods getReplyIndex and getPageNumber and you could get the page number by a reply id.Areta
Yeah I'm really not trying to get result sets here, I just need an integer. Laravel's paginator loads the correct page by simply having its integer in the URL - so if I go to posts?page=2, it'll show that result set automatically.Satisfied
Okay, I combined getPageNumber and getReplyIndex into one method and it's returning a double. I'm using two per page as an example, with 4 entries I get 2.5 as the resultSatisfied
I wrapped the return value in CEIL() and that seems to have done the trick. Again, I think where I was really getting confused earlier was when your methods were returning pages of posts. It's pretty cool that Laravel can figure this out by simply supplying an integer. Even cooler is the fact that decimals actually work - with 4 posts paginated to 2 per page, page 1.5 shows one postSatisfied
@Satisfied That is cool. The problem in the function was that the index was being to cast before the equation was executed... I wrapped the equation in parenthesis, and now it works as expected.Areta
about pagination... how it works in Laravel? If I am on page 3 of 10 items (21-31) and I want to load page 2 too (in an ajax call), if an user delete a relevant post meanwhile, will it cause duplicates loading?Scarcity
$page_number = ceil($current_index/$per_page);Scarcity
This is strictly based on IDs auto-increment. Is there a way to make this work when we apply ordering? How can we efficiently get the index of a row by ID when ordering is applied?Ashram
S
2

Here is what I came up with. If anyone has any suggestions to improve on this, PLEASE let me know. I'm really wondering if there is a more Laravel way to do this and I would really appreciate Jeffrey Way sharing his secret, since he is doing this exact thing over at Laracasts.

/**
     * Get Reply Page
     * 
     * Returns the page number where a reply resides as it relates to pagination
     * 
     * @param null $replyId Optional ID for specific reply
     * @param bool $pageLink If True, return a GET parameter ?page=x
     * @param int $paginate Number of posts per page
     * @return int|null|string // Int for only the page number, null if no replies, String if $pageLink == true
     */
    public function getReplyPage($replyId = null, $pageLink = false, $paginate = 20)
    {
        // Find the page for a specific reply if provided, otherwise find the most 
        // recent post's ID. If there are no replies, return null and exit.
        if (!$replyId) {
            $latestReply = $this->latestReply();
            if ($latestReply) {
                $replyId = $latestReply->id;
            } else {
                return null;
            }
        }

        // Find the number of replies to this Post prior to the one in question
        $count = $this->replies()->where('id', '<', $replyId)->count();

        $page = CEIL($count / $paginate +1);

        // Return either the integer or URL parameter
        return $pageLink ? "?page=$page" : $page;
    }
Satisfied answered 20/3, 2015 at 16:17 Comment(2)
From a conceptual level, I see no reason a model should return a link- unless, of course, it is the model for a table that tracks a bunch of links. Also, auto-assigning every argument is dangerous territory. Define a rigid interface that everything has to follow, and you will thank yourself later. Finally, there no reason to use a for loop in this situation. You have integer division for this kind of problem.Areta
Appreciate the thoughts. It's far from perfect (the for loop was late night thinking, of course division was the better solution). What I mostly care about is knowing which page a reply resides on so that I can have a link directly to that reply - which is why I included it in the method. It helped cut down on the amount of inline code to accomplish that goal.Satisfied

© 2022 - 2024 — McMap. All rights reserved.