<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('daily_order_summaries', function (Blueprint $table) {
$table->id();
$table->date('summary_date');
$table->string('currency', 3);
$table->unsignedInteger('orders_count')->default(0);
$table->decimal('gross_total', 15, 2)->default(0);
$table->timestamps();
$table->unique(['summary_date', 'currency']);
});
}
public function down(): void
{
Schema::dropIfExists('daily_order_summaries');
}
};
<?php
namespace App\Jobs;
use App\Models\DailyOrderSummary;
use App\Models\Order;
use Carbon\CarbonImmutable;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class RollUpDailyOrders implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, SerializesModels;
public int $tries = 3;
public array $backoff = [30, 120, 300];
public function __construct(public CarbonImmutable $date)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping('rollup:' . $this->date->toDateString()))->expireAfter(600)];
}
public function handle(): void
{
if ($this->batch()?->cancelled()) {
return;
}
$rows = Order::query()
->whereBetween('paid_at', [$this->date->startOfDay(), $this->date->endOfDay()])
->where('status', 'paid')
->selectRaw('currency, COUNT(*) as orders_count, SUM(amount) as gross_total')
->groupBy('currency')
->get();
foreach ($rows as $row) {
DailyOrderSummary::updateOrCreate(
['summary_date' => $this->date->toDateString(), 'currency' => $row->currency],
['orders_count' => (int) $row->orders_count, 'gross_total' => $row->gross_total]
);
}
}
}
<?php
namespace App\Console\Commands;
use App\Jobs\RollUpDailyOrders;
use Carbon\CarbonImmutable;
use Illuminate\Bus\Batch;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Throwable;
class DispatchDailyRollups extends Command
{
protected $signature = 'orders:roll-up {--days=7}';
protected $description = 'Aggregate paid orders into the daily summary table';
public function handle(): int
{
$end = CarbonImmutable::yesterday();
$start = $end->subDays((int) $this->option('days') - 1);
$jobs = collect($start->toPeriod($end)->toArray())
->map(fn (CarbonImmutable $date) => new RollUpDailyOrders($date));
$batch = Bus::batch($jobs->all())
->name('Daily order rollup ' . $start->toDateString() . '..' . $end->toDateString())
->onQueue('reports')
->then(function (Batch $batch) {
Log::info('Rollup complete', ['batch' => $batch->id, 'jobs' => $batch->totalJobs]);
})
->catch(function (Batch $batch, Throwable $e) {
Log::error('Rollup failed', ['batch' => $batch->id, 'error' => $e->getMessage()]);
})
->finally(function (Batch $batch) {
Log::info('Rollup finished', ['batch' => $batch->id, 'failed' => $batch->failedJobs]);
})
->dispatch();
$this->info("Dispatched batch {$batch->id} with {$batch->totalJobs} day(s).");
return self::SUCCESS;
}
}
This snippet shows how to build a fault-tolerant daily rollup that aggregates raw orders into a daily_order_summaries table using Laravel's job batching feature. The pattern splits one large aggregation into many small, independent jobs — one per date — that run in parallel across queue workers, then fires a completion callback once the whole batch finishes. This scales far better than a single long-running command, which risks memory blowups and timeouts, and gives per-day retry granularity so one bad day does not fail the entire run.
The summaries migration defines the target table. A composite unique index on summary_date keeps the rollup idempotent: re-running a day must not create duplicate rows. The monetary column uses decimal rather than a float to avoid rounding drift when summing thousands of orders, and orders_count lets reports distinguish a zero-revenue day from a missing one.
In RollUpDailyOrders job, each instance owns exactly one Carbon date. The handle method groups that day's orders by currency and writes results with updateOrCreate, keyed on the unique columns, so the job is safe to retry. The WithoutOverlapping middleware keyed on the date prevents two workers from processing the same day concurrently, and $backoff staggers retries. Because each job touches only one date, its working set stays tiny regardless of total volume.
The DispatchDailyRollups command is the orchestrator. It builds the date range, maps each date to a job, and hands the collection to Bus::batch(...). The fluent then, catch, and finally callbacks attach behavior to the batch lifecycle: then runs only when every job succeeds, catch fires on the first failure, and finally always runs for cleanup and metrics. allowFailures() is deliberately omitted so a failed day marks the batch as failed rather than silently skipping data.
A subtle pitfall worth noting: batch callbacks are serialized, so they should close over scalars like $batch->id rather than heavy objects. The onQueue and name calls make the batch observable in Horizon and the job_batches table, which matters when a nightly rollup quietly stops producing numbers. Reach for this approach whenever a periodic aggregation grows too large for one process but decomposes cleanly along a natural key such as a date or tenant.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]
sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first
SQL injection prevention with unsafe and safe query patterns
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
-- EXPLAIN ANALYZE (actual execution statistics)
EXPLAIN ANALYZE
SELECT u.username, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'
Advanced query optimization techniques
Share this code
Here's the card — post it anywhere.