When I started working with Laravel, combining GroupBy with pagination seemed tricky. Laravel makes it easier to handle complex queries, but grouping data while maintaining pagination can still be a challenge.
In this article, I’ll walk you through how to implement GroupBy
with pagination in Laravel 12 using a clear, real-world example. Whether you're building a blog, e-commerce platform, or a custom dashboard, this guide will help you display grouped data efficiently with pagination.
First, ensure you have a Laravel 12 project set up. If you don’t, you can create one using Composer:
composer create-project laravel/laravel groupby-pagination-example
After setting up, configure your database in the .env
file and run migrations to create the necessary tables.
For this example, let’s assume we’re building a system to display grouped orders by customers. We’ll use two models: Customer
and Order
.
Run the following commands to create the models and migrations:
php artisan make:model Customer -m
php artisan make:model Order -m
Update the migration files in database/migrations
:
For customers
table:
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
For orders
table:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->onDelete('cascade');
$table->string('product_name');
$table->decimal('amount', 8, 2);
$table->timestamps();
});
Run the migrations:
php artisan migrate
To test the functionality, let’s seed some sample data. Create a seeder:
php artisan make:seeder CustomerOrderSeeder
Edit database/seeders/CustomerOrderSeeder.php
:
namespace Database\Seeders;
use App\Models\Customer;
use App\Models\Order;
use Illuminate\Database\Seeder;
class CustomerOrderSeeder extends Seeder
{
public function run()
{
$customers = [
['name' => 'John Doe', 'email' => '[email protected]'],
['name' => 'Jane Smith', 'email' => '[email protected]'],
['name' => 'Bob Johnson', 'email' => '[email protected]'],
];
foreach ($customers as $customerData) {
$customer = Customer::create($customerData);
Order::create([
'customer_id' => $customer->id,
'product_name' => 'Product A',
'amount' => rand(10, 100) . '.99',
]);
Order::create([
'customer_id' => $customer->id,
'product_name' => 'Product B',
'amount' => rand(10, 100) . '.99',
]);
}
}
}
Run the seeder:
php artisan db:seed --class=CustomerOrderSeeder
Create a controller to handle the query logic:
php artisan make:controller OrderController
Edit app/Http/Controllers/OrderController.php
:
namespace App\Http\Controllers;
use App\Models\Order;
use App\Models\Customer;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function index(Request $request)
{
$orders = $this->getOrdersQuery($request);
return view('orders.index', compact('orders'));
}
private function getOrdersQuery(Request $request)
{
$query = Order::query()
->selectRaw('max(orders.id) as order_id')
->join('customers', 'orders.customer_id', '=', 'customers.id')
->orderByDesc('orders.id');
if ($request->has('search') && $request->search) {
$query->where('customers.name', 'like', '%' . $request->search . '%')
->orWhere('customers.email', 'like', '%' . $request->search . '%');
}
if ($request->has('status') && $request->status) {
if ($request->status == 'high_value') {
$query->where('orders.amount', '>=', 50);
} elseif ($request->status == 'low_value') {
$query->where('orders.amount', '<', 50);
}
}
$result = $query->groupBy('customers.id')->paginate(5);
$orders = Order::query()
->select('orders.*', 'customers.name as customer_name', 'customers.email')
->join('customers', 'orders.customer_id', '=', 'customers.id')
->whereIn('orders.id', $result->pluck('order_id')->toArray())
->get();
return ['orders' => $orders->groupBy('customer_id'), 'result' => $result];
}
}
In routes/web.php
, add the route:
use App\Http\Controllers\OrderController;
Route::get('/orders', [OrderController::class, 'index'])->name('orders.index');
Create a view file at resources/views/orders/index.blade.php
:
<!DOCTYPE html>
<html>
<head>
<title>Orders with Pagination</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Customer Orders</h1>
<!-- Search and Filter Form -->
<form method="GET" action="{{ route('orders.index') }}" class="mb-4">
<div class="row">
<div class="col-md-4">
<input type="text" name="search" class="form-control" placeholder="Search by name or email" value="{{ request('search') }}">
</div>
<div class="col-md-4">
<select name="status" class="form-control">
<option value="">All Orders</option>
<option value="high_value" {{ request('status') == 'high_value' ? 'selected' : '' }}>High Value (> $50)</option>
<option value="low_value" {{ request('status') == 'low_value' ? 'selected' : '' }}>Low Value (< $50)</option>
</select>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
</div>
</form>
<!-- Orders Display -->
@forelse ($orders['orders'] as $customer_id => $customerOrders)
@php
$firstOrder = $customerOrders->first();
$customerName = $firstOrder->customer_name;
@endphp
<div class="card mb-3">
<div class="card-header">
<h5>{{ $customerName }} ({{ $customerOrders->count() }} Orders)</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Product</th>
<th>Amount</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@foreach ($customerOrders as $order)
<tr>
<td>{{ $order->product_name }}</td>
<td>${{ $order->amount }}</td>
<td>{{ $order->created_at->format('M d, Y') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@empty
<div class="card">
<div class="card-body">
No Orders Found
</div>
</div>
@endforelse
<!-- Pagination Links -->
{{ $orders['result']->links('pagination::bootstrap-5') }}
</div>
</body>
</html>
Run the Laravel development server:
php artisan serve
Visit http://localhost:8000/orders
to see the grouped orders with pagination. You can use the search and filter options to test the functionality.
Implementing GroupBy
with pagination in Laravel 12 is straightforward once you break it down into steps. By structuring your query to handle grouping and pagination separately, you can efficiently display grouped data while maintaining a user-friendly interface. This approach is perfect for dashboards, reports, or any application where you need to group related records. With the example above, you can adapt the code to fit your project’s needs, whether it’s for orders, subscriptions, or other data types.
1. Why do we need two queries for GroupBy with pagination?
Laravel’s groupBy
doesn’t work directly with pagination in a single query due to SQL limitations. Using two queries—one for grouping and pagination, and another to fetch detailed records—ensures compatibility and performance.
2. Can I use Eloquent relationships instead of joins?
Yes, you can use Eloquent relationships to fetch related data, but for complex grouping, joins are often more efficient. Ensure you eager-load relationships to avoid N+1 query issues.
3. How do I handle large datasets with this approach?
For large datasets, optimize your database with indexes on frequently queried columns (e.g., customer_id
, created_at
). You can also use caching to improve performance.
4. Can I customize the pagination style?
Yes, Laravel allows you to publish and customize pagination views. Run php artisan vendor:publish --tag=laravel-pagination
to modify the pagination templates.
5. Is this approach compatible with earlier Laravel versions?
This approach works in Laravel 9, 10, and 11 as well, with minor adjustments to pagination view paths or syntax based on the version.
You might also like :