likvidatorTM's avatar

Pagination with dinamic data.

Hi, recently i used eloquent simple pagination but now i found some critical bug (eloquent method works fine, bug in my solution=)). In query i am use some conditions like if order active, if user can see this order end etc. When user gets next page results can be displaced and he gots incorrect data. For example: user loads first page with 10 orders then he loads second page and 8 of recent orders was marked as "disabled" and response data will contains 2 orders that user already loaded. I cant use coursor pagination because sort field not unique. I see only 1 way to resolve this problem: send from client orders ids that he loaded and then use its in query where NOT IN. P/S its a SPA application.

0 likes
6 replies
likvidatorTM's avatar

Migration:

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->engine = 'InnoDB';
        $table->increments('id');
        $table->unsignedInteger('amount');
        $table->string('name', 30)->nullable();
        $table->enum('type', ['full-price', 'cut-price']);
        $table->timestamps();
    });
}

Controller:

public function index(Product $product, int $page = 1, string $filter = NULL)
{
    if ($filter && !in_array($filter, ['full-price', 'cut-price']))
        $filter = NULL;
    $page = (int)$page ?: 1;

    \Illuminate\Pagination\Paginator::currentPageResolver(function () use ($page) {
        return $page;
    });

    $paginator = $product->AvailableByPage($filter);
    $products['data'] = $paginator->items();
    $products['hasMore'] = $paginator->hasMorePages();
    if($page === 1)
        $products['total'] = $product->AvailableCount($filter);

    return $this->defaultResponse([
        'products' => [
            'items' => $products,
        ]]);
}

Model:

public function AvailableByPage(?string $filter)
{
    return Product::Select('*')
        ->if($filter, 'type', '=', $filter)
        ->where('amount', '>', 0)
        ->orderBy('amount', 'DESC')
        ->withSomethingElse()
        ->simplePaginate($this->items_per_page);
}

P/S its just example.

gregrobson's avatar

Have a look at [forPageAfterId()[(https://github.com/laravel/framework/blob/5.5/src/Illuminate/Database/Query/Builder.php#L1549)

MyModel::forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')

Say your first page loads (5 per page) and the IDs are 4, 6, 7, 9 and 12. You click next, but in the last few seconds another user deleted record 7. When you click next you would normally miss the record after 12 (as that now falls on page 1). Instead use forPageAfterId();

$lastId = 12; // last value from previous page
MyModel::select('*')
    ->forPageAfterId(5, $lastId)
    ->get();

Even though there are only 4 items on page 1, you still see the first 5 after the id of 12.

This will only work if paginating on the id column, but the principle is the same. In your code you need to track the last result of the current page (or first if navigating backwards) and whenever you requery add in an extra filter that says "the first X records that meet the criteria but start at X".

likvidatorTM's avatar

We can't use this principle because order column ISN'T unique! I forget add order condition in example sorry.

aurawindsurfing's avatar

Your code looks too complicated to me. Why are you creating paginator instance? I would just put a paginator in the controller where I call the model and before I send it to the view like so:

$products = Product::Select('*')
        ->if($filter, 'type', '=', $filter)
        ->where('amount', '>', 0)
        ->orderBy('amount', 'DESC')
        ->withSomethingElse()
        ->simplePaginate($this->items_per_page);

return view('products', compact('products));

and nothing else.

You do not have to handle all paginator stuff since it is done automatically by Laravel.

Hope I got the question right.

likvidatorTM's avatar

Because current page parameter user send with route argument and Paginator dont see it by default. But it doesn't matter since this is not the reason of problem. Route path: xhr/products/page/{page}/{filter?}

Please or to participate in this conversation.