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);
}
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();
});
}
}
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'],
];
}
}
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);
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,
],
];
}
}
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)