28
Nov '20

One of the sites I run is a craft business where we want to have a paginated product listing on the front page, but we also want it to be randomised, so that you don’t always get the same products when you first load it.

I wasn’t even sure this was possible when I began looking into it, but thanks to PHP’s ability to seed the randomisation functions, and a bit of careful tapping into Laravel’s core pagination library it is possible.

I’ve split the logic into two functions. One that generates a repeatably randomised set of product records, and another that gives the ability to use Laravel’s paginator on a Collection instance, rather than a query builder instance:

<?php

use Illuminate\Support\Collection;
use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\LengthAwarePaginator;

class ProductController extends \Controller
{
    /**
     * Product index page
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('product.index')->with('title', 'Products')
                    ->with('products', $this->randomisedPaginatedProducts());
    }

    /**
     * Get randomised paginated products
     * @return \Collection
     */
    protected function randomisedPaginatedProducts() {

        if(session()->has('seed'))
            $seed = session('seed');
        else {
            $seed = rand(1, 100);
            session()->put('seed', $seed);
        }

        mt_srand($seed);

        $records = \App\Models\Product::get();

        $range = range(0, $records->count() - 1);
        shuffle($range);

        $randomised = collect($range)->map(function($index) use ($records) {
            return $records[$index];
        });

        return $this->paginateCollection($randomised);
    }

    /**
     * Paginate collection
     *
     * @param Collection $items
     * @param int $per_page
     * @param int|null $page
     * @param array $options (path, query, fragment, pageName)
     * @return LengthAwarePaginator
     */
    protected function paginateCollection(Collection $items, int $per_page = null, int $current_page = null, array $options = [])
    {
        $current_page = $current_page ?: (Paginator::resolveCurrentPage() ?: 1);

        return new LengthAwarePaginator($items->forPage($current_page, $per_page), $items->count(), $per_page, $current_page, $options);
    }
}

The key to this is the seed that we generate and keep in the session (we could equally pass it around in the pagination links if we wanted). The seed here is a number between 1 and 100, but it could be anything, really this just means that there are 100 different combinations of products it could come up with. This seed is used to generate a randomised array of numbers which will correspond to the numeric keys of the list of products, and then the products are sorted into a new array accordingly. Pass these to the paginator and you’re done.

Do be aware that this method requires you to pull all the products out of the database upfront, but for a relatively small total product count it’s no issue.