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.
- GitHub: @osteel
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:
- When to declare classes final - Ocramius
- PHP Manual - final
- PHP 8.1 readonly properties - PHP.Watch
- PHP 8.2 readonly classes - PHP.Watch
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:
- PHP version upgrades: automatically modernise code from older PHP syntax to newer equivalents (typed properties, attributes, readonly, etc.)
- 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:
- rectorphp/rector on GitHub
- Rector integration guide for new projects
- driftingly/rector-laravel
- Rector on Laravel with Laracasts - Defensive Laravel series
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:
- Generics by Examples - PHPStan
- Generics in PHP using PHPDocs - PHPStan blog
- Defensive Laravel series (Generics episode) - Laracasts
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.