Originally published on decentphp.dev — part of the Writing decent PHP course. View this lesson on the original site.

Rules and invariants you can rely on

An invariant is a rule that must always be true.
If those rules are vague or spread around the codebase, bugs slip through and reviews become guesswork.

Before

<?php

function updateInvoice(array $input, $invoiceRepository): void
{
    $status = $input['status'] ?? '';
    $totalInPence = (int) ($input['total_in_pence'] ?? 0);

    // accepts any status string and allows negative totals
    $invoiceRepository->save([
        'id' => (int) $input['id'],
        'status' => $status,
        'total_in_pence' => $totalInPence,
    ]);
}

After

<?php

const ALLOWED_INVOICE_STATUSES = ['draft', 'sent', 'paid', 'void'];

function updateInvoice(array $input, $invoiceRepository): void
{
    $status = (string) ($input['status'] ?? '');
    if (! in_array($status, ALLOWED_INVOICE_STATUSES, true)) {
        return;
    }

    $totalInPence = (int) ($input['total_in_pence'] ?? 0);
    if ($totalInPence < 0) {
        return;
    }

    $invoiceRepository->save([
        'id' => (int) $input['id'],
        'status' => $status,
        'total_in_pence' => $totalInPence,
    ]);
}

Why this version is better

  • the allowed rules are explicit and easy to find
  • impossible data is rejected early instead of persisted
  • other developers can rely on the saved data shape with less defensive code

From Writing decent PHP — What decent PHP looks like