DEV Community

Cover image for How I Write Laravel Code That Future-Me Can still Read.
Christos Koumpis
Christos Koumpis

Posted on

How I Write Laravel Code That Future-Me Can still Read.

I stopped chasing clever solutions and started optimizing for one thing: Can I understand this quickly 1 year later?

That one question helped me build a set of rules I follow when writing code.

1) Keep Controllers thin.

If a Controller method starts doing validation, business logic, mapping, response etc. Its doing too much work.

So my controllers changed to:
Request in -> Action Call -> Response out.

public function store(StoreInvoiceRequest $request, CreateInvoiceAction $action)
{
    $invoice = $action->execute($request->validated());

    return InvoiceResource::make($invoice);
}
Enter fullscreen mode Exit fullscreen mode

2) Move business logic into Actions/Services.

When logic matters to the domain, I don't bury it in controllers or models.

It belongs in a dedicated class with a clear responsibility.

final class CreateInvoiceAction
{
    public function execute(array $data): Invoice
    {
        return DB::transaction(function () use ($data) {
            $invoice = Invoice::create([
                'customer_id' => $data['customer_id'],
                'due_date' => $data['due_date'],
                'status' => 'draft',
            ]);

            foreach ($data['items'] as $item) {
                $invoice->items()->create([
                    'description' => $item['description'],
                    'qty' => $item['qty'],
                    'unit_price' => $item['unit_price'],
                ]);
            }

            return $invoice->refresh();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

3) Use Form Requests for validation.

I avoid inline validation in controllers unless its truly tiny.

final class StoreInvoiceRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'customer_id' => ['required', 'exists:customers,id'],
            'due_date' => ['required', 'date'],
            'items' => ['required', 'array', 'min:1'],
            'items.*.description' => ['required', 'string', 'max:255'],
            'items.*.qty' => ['required', 'integer', 'min:1'],
            'items.*.unit_price' => ['required', 'numeric', 'min:0'],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

4) Prefer explicit queries over "magic"

Eloquent is a powerful tool, but unclear query chains can become painful really fast.

That's why I :

  • Scope frequently used filters.
  • Use eager loading.
  • Select only needed columns for heavy endpoints.
$invoices = Invoice::query()
    ->with(['customer:id,name', 'items:id,invoice_id,qty,unit_price'])
    ->where('status', 'sent')
    ->whereDate('due_date', '<', now())
    ->latest('due_date')
    ->paginate(20);

Enter fullscreen mode Exit fullscreen mode

5) Use Resources for API Responses.

There is no need to return raw models from APIs.
JsonResource keeps response structured and stable.

final class InvoiceResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'number' => $this->number,
            'status' => $this->status,
            'total' => $this->total,
            'customer' => [
                'id' => $this->customer->id,
                'name' => $this->customer->name,
            ],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

6) Better naming.

I believe intent based names are self documented.

Examples:
CalculateInvoiceTotals
SendReminder

7) Use transactions for multi step writes.

If multiple records must succeed, then I use DB::transaction().

8) Comment decisions, not syntax.

Example:
// Use immutable snapshot so invoice totals don't change if product prices update later

That kind of comment saves me real debugging time.

Always looking to improve this system. What are you doing in your projects that consistently works?

Top comments (0)