<?php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Services\InvoicePdfService;
use Illuminate\Http\Response;
class InvoiceController extends Controller
{
public function __construct(private InvoicePdfService $invoices)
{
}
public function show(Order $order): Response
{
$this->authorize('view', $order);
$dompdf = $this->invoices->render($order);
$filename = $this->invoices->filename($order);
return response($dompdf->output(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . $filename . '"',
'Cache-Control' => 'private, no-store',
]);
}
}
<?php
namespace App\Services;
use App\Models\Order;
use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Support\Facades\View;
class InvoicePdfService
{
public function render(Order $order): Dompdf
{
$order->loadMissing(['items.product', 'customer']);
$options = new Options();
$options->set('isRemoteEnabled', true);
$options->set('defaultFont', 'DejaVu Sans');
$dompdf = new Dompdf($options);
$dompdf->setPaper('A4', 'portrait');
$html = View::make('invoices.invoice', ['order' => $order])->render();
$dompdf->loadHtml($html);
$dompdf->render();
return $dompdf;
}
public function filename(Order $order): string
{
return 'invoice-' . $order->number . '.pdf';
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: 'DejaVu Sans', sans-serif; font-size: 12px; color: #222; }
.header { border-bottom: 2px solid #333; padding-bottom: 8px; margin-bottom: 16px; }
.header h1 { margin: 0; font-size: 22px; }
table { width: 100%; border-collapse: collapse; margin-top: 12px; }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #ddd; }
td.amount, th.amount { text-align: right; }
.total { font-weight: bold; font-size: 14px; }
</style>
</head>
<body>
<div class="header">
<h1>Invoice #{{ $order->number }}</h1>
<div>Billed to: {{ $order->customer->name }}</div>
<div>Date: {{ $order->created_at->format('M j, Y') }}</div>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th class="amount">Qty</th>
<th class="amount">Unit</th>
<th class="amount">Total</th>
</tr>
</thead>
<tbody>
@foreach ($order->items as $item)
<tr>
<td>{{ $item->product->name }}</td>
<td class="amount">{{ $item->quantity }}</td>
<td class="amount">${{ number_format($item->unit_price, 2) }}</td>
<td class="amount">${{ number_format($item->quantity * $item->unit_price, 2) }}</td>
</tr>
@endforeach
<tr class="total">
<td colspan="3" class="amount">Total</td>
<td class="amount">${{ number_format($order->total, 2) }}</td>
</tr>
</tbody>
</table>
</body>
</html>
This snippet shows a focused, production-shaped way to render an order into a PDF invoice and push it straight to the browser without ever writing a temporary file to disk. The work is split across three collaborating files: a service that builds the PDF, a Blade template that defines its layout, and a thin controller that authorizes the request and streams the bytes.
In InvoicePdfService, the rendering logic lives behind a single render(Order $order) method that returns a Dompdf instance. Isolating Dompdf here keeps the controller ignorant of the PDF engine, so swapping libraries or adding a caching layer later touches only one class. The service eager-loads items.product and customer up front to avoid N+1 queries while the template iterates, computes a filename() from the order number, and configures Dompdf with isRemoteEnabled so the template can reference a logo over HTTP. The HTML is produced by rendering a Blade view to a string via view(...)->render(), which lets the invoice reuse the framework's templating instead of concatenating markup by hand.
The invoice.blade.php tab is a self-contained print layout. Dompdf understands a practical subset of CSS, so styles are kept inline in a <style> block rather than pulled from an external stylesheet, and layout leans on tables because Dompdf's flex/grid support is limited. It formats currency with number_format, escapes user data through Blade's {{ }} syntax, and renders the line items in a loop.
The InvoiceController ties it together. show() uses route-model binding to receive the Order, calls authorize('view', $order) so only the owner can pull an invoice, then delegates to the service. Rather than Dompdf::stream() (which calls exit), it returns a Laravel Response built from $dompdf->output(), setting Content-Type: application/pdf and a Content-Disposition of inline so the browser previews the file; switching to attachment would force a download instead. Returning a real response object keeps middleware, testing, and exception handling intact, which raw streaming would bypass. The dompdf->output() approach buffers the whole document in memory, a reasonable trade-off for invoices but something to reconsider for very large documents, where a queued job writing to storage would fit better.
Related snips
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
import type { IncomingMessage, ServerResponse } from "http";
const MIN_BYTES = 1024;
const INCOMPRESSIBLE = /^(image|video|audio)\/|application\/(zip|gzip|x-brotli|pdf|octet-stream)/i;
Response compression (only when it helps)
Share this code
Here's the card — post it anywhere.