← Back to writing
6 min read

Escaping the Code Maze

Laravel Php Learning

Yannick Chenot (Yellow Raincoat Limited) is a senior backend developer based in Brighton, UK, contracting via his company Yellow Raincoat Limited. He has given versions of this talk at the UK Symfony Meetup under the title "Closed-by-Default Principle". His background includes Laravel, PHP, Docker, and DDD.

The core idea: remove assumptions from code

The talk is built around reducing the number of implicit assumptions in a codebase. Every place where code could take an unexpected path, accept an unexpected value, or be extended in an unexpected direction is a source of bugs and cognitive overhead. The tools and techniques below address this by making constraints explicit and machine-enforceable rather than relying on convention or documentation.

The framing Yannick uses is "Closed-by-Default": making code closed to unexpected use unless explicitly opened, rather than open by default and hoping no one misuses it.

final, typed variables, and readonly parameters

Three PHP language features that reduce the surface area for assumptions.

final prevents a class from being extended or a method from being overridden. A final class has a fixed, knowable set of behaviours. Static analysis tools like PHPStan can make stronger inferences about final classes because they know no subclass can change the behaviour. A useful rule of thumb: if a class implements an interface and has no public methods beyond those defined in the interface, it should be final.

final class EmailNotification implements Notification
{
    public function __construct(
        private readonly string $recipient,
    ) {}
}

Typed parameters and return types give PHPStan and Larastan the information they need to check call sites and catch type mismatches before runtime. The more specific the types (e.g. Email value object rather than string), the more the analyser can verify.

readonly (PHP 8.1+) prevents a property from being modified after construction. A readonly class (PHP 8.2+) applies this to every property automatically. Combined with final, this produces objects that are fully immutable and have no mutation surface for either runtime code or subclasses to exploit.

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}
}

When all three are used together on value-carrying objects, PHPStan can reason exhaustively about all possible states the object can be in.

References:

Applying Rector to existing projects

Rector is an automated refactoring tool that parses PHP into an abstract syntax tree, applies transformation rules, and writes the modified code back to disk. It is not a linter; it makes actual changes.

Two primary uses:

  1. PHP version upgrades: automatically modernise code from older PHP syntax to newer equivalents (typed properties, attributes, readonly, etc.)
  2. Code quality enforcement: apply rules like removing dead code, adding type declarations, enforcing early returns, converting to stricter boolean handling

For applying to an existing project, the recommended approach is incremental. Start with --dry-run to review what would change, then enable a small ruleset, apply it, run tests, commit, and repeat. Enabling everything at once produces large unreviewable diffs.

composer require rector/rector --dev
composer require driftingly/rector-laravel --dev

# Preview changes without applying
vendor/bin/rector --dry-run

# Apply
vendor/bin/rector

Example rector.php for a Laravel project:

RectorConfig::configure()
    ->withPaths([__DIR__ . '/app', __DIR__ . '/database', __DIR__ . '/routes'])
    ->withPhpSets()
    ->withPreparedSets(
        deadCode: true,
        codeQuality: true,
        typeDeclarations: true,
        earlyReturn: true,
        strictBooleans: true,
    );

Rector and PHPStan are complementary: PHPStan finds problems, Rector fixes them. Running both in CI catches regressions continuously.

References:

PHPStan generics by example

PHP does not have native generics, but PHPStan supports them via PHPDoc annotations using @template. This allows typed collections and service locator patterns where the return type depends on the input type.

The core tag is @template T, which declares a type variable. Once declared, it can be used in @param and @return annotations to link the types together:

/**
 * @template T
 * @param T $value
 * @return T
 */
function identity(mixed $value): mixed
{
    return $value;
}

A more practical example, a typed collection:

/**
 * @template T
 */
class Collection
{
    /** @var T[] */
    private array $items = [];

    /** @param T $item */
    public function add(mixed $item): void
    {
        $this->items[] = $item;
    }

    /** @return T|null */
    public function first(): mixed
    {
        return $this->items[0] ?? null;
    }
}

/** @var Collection<Invoice> $invoices */
$invoices = new Collection();
$invoices->add(new Invoice()); // PHPStan knows this must be an Invoice
$invoice = $invoices->first(); // PHPStan knows this is Invoice|null

When a class implements a generic interface, use @implements to specify the type:

/** @implements Collection<User> */
class UserCollection extends Collection {}

The PHPStan documentation page "Generics by Examples" is the most practical reference; it collects real-world scenarios in one place rather than explaining the theory from scratch.

References:

Static analysis to herd AI-generated code

Static analysis is an effective enforcement layer on top of AI-generated code. Because AI pattern matches against existing code quality and does not understand intent, it can produce code that is syntactically valid but violates type contracts, architectural boundaries, or project-specific conventions.

PHPStan and Larastan will flag type errors, incorrect method signatures, undefined variables, and misused generics in AI-generated PHP regardless of where the code came from. Bladestan extends this to Blade templates. Rector can enforce structural rules automatically.

The connection to the earlier points in the talk: if the codebase already uses final, typed parameters, and readonly, then PHPStan has more information to work with and catches a broader class of errors in AI output. A well-typed codebase is harder for AI to produce subtly wrong code in because the type system leaves fewer gaps.

This point aligns with Aaron Francis's position from the same conference: deterministic checks enforce standards on AI output more reliably than instruction files.

References:


Notes from Laravel Live UK 2026.