NightFox

NightFox

Object-Oriented PHP for Real Applications

I like objects when data and the rules about that data genuinely belong together. What I don't like is turning everything into a class because that's what "real" programmers supposedly do. The point of OOP is clear boundaries you can enforce, not a folder full of AbstractFactoryManagers.

A small domain object

<?php
declare(strict_types=1);

final class Invoice
{
    /** @var list<InvoiceLine> */
    private array $lines = [];

    public function __construct(
        private readonly int $id,
        private string $status = 'draft'
    ) {
    }

    public function addLine(InvoiceLine $line): void
    {
        if ($this->status !== 'draft') {
            throw new DomainException('Only draft invoices may be changed.');
        }

        $this->lines[] = $line;
    }

    public function totalCents(): int
    {
        return array_sum(array_map(
            static fn (InvoiceLine $line): int => $line->totalCents(),
            $this->lines
        ));
    }
}

final readonly class InvoiceLine
{
    public function __construct(
        public string $description,
        public int $quantity,
        public int $unitPriceCents
    ) {
        if ($quantity < 1 || $unitPriceCents < 0) {
            throw new InvalidArgumentException('Invalid invoice line.');
        }
    }

    public function totalCents(): int
    {
        return $this->quantity * $this->unitPriceCents;
    }
}

Composition over inheritance

Inheritance welds two classes together for good. Composition is usually more flexible: hand the service the mailer, clock, repository, or pricing policy it needs, and swap them out whenever you want.

<?php
declare(strict_types=1);

interface Clock
{
    public function now(): DateTimeImmutable;
}

final class SystemClock implements Clock
{
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable('now', new DateTimeZone('UTC'));
    }
}

Dependency injection without a container

<?php
declare(strict_types=1);

$pdo = Database::connect();
$customers = new PdoCustomerRepository($pdo);
$auditLog = new PdoAuditLog($pdo);
$suspendCustomer = new SuspendCustomer($customers, $auditLog);

$controller = new CustomerController($suspendCustomer);

You can bring in a DI container later to wire everything up automatically. Starting out by building objects by hand keeps it obvious what depends on what, and honestly, a lot of apps never need more than that.

Interfaces at real boundaries

Interfaces earn their keep where the implementation really might change, or where faking it makes testing easier: payment gateways, mail delivery, object storage, clocks, queues, repositories. An interface with exactly one implementation that you never swap or fake is just one more file to open.

Value objects

<?php
declare(strict_types=1);

final readonly class EmailAddress
{
    public string $value;

    public function __construct(string $value)
    {
        $value = mb_strtolower(trim($value));

        if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException('Invalid email address.');
        }

        $this->value = $value;
    }
}

Once a value object exists, you know it's valid. That means you can stop re-checking the same email address in five different places. This is probably my favorite OOP habit.

Last Modified: October 12, 2026, 11:46 am