Appearance
Tier 2b · Chapter 1: Modern PHP Foundations
PHP/Laravel Track · ~30 min read + exercise
Why this chapter exists
The Foundations track gave you the architectural vocabulary; this track makes it concrete in PHP and Laravel. We start with the language itself, because modern PHP is a far better foundation for clean architecture than its reputation suggests — and because the habits you bring to PHP determine whether the architecture you design survives contact with the codebase. PHP earned a reputation in its early years for loose typing and sprawling, hard-to-reason-about code, and a lot of engineers' mental model of the language is frozen there. Modern PHP (8.x) is a different language in the ways that matter for architecture: it has real type declarations, immutable value objects, enums, and the tooling to enforce them. This chapter is about using those tools to make your boundaries and domain concepts as solid in PHP as the foundations call for — pushing correctness into types so the structure you design is enforced by the language, not merely intended.
The throughline: modern PHP gives you the tools to make structure explicit and enforced — types at boundaries, immutable values, meaningful domain types — and clean architecture in PHP starts with actually using them. The trap is writing PHP as if it were still 2010: untyped arrays passed everywhere, primitive obsession (everything a string or int), mutable state changing under you. Each of those is a place the structure you designed dissolves into soup. The discipline of this chapter is the foundations' type-safety idea in PHP form — making the language's type system carry your architectural intent.
What this chapter covers
- Types as architecture — PHP's type declarations and why using them fully is a structural decision.
- Value objects and immutability — modeling domain concepts as small, immutable types instead of primitives.
- Avoiding primitive obsession and array-soup — meaningful types over
string/int/arrayeverywhere. - Enforcing it — static analysis as the PHP fitness function that keeps the discipline honest.
Types as architecture
PHP's type system is optional — you can write untyped PHP — and that optionality is exactly why using it fully is an architectural choice rather than a given. Modern PHP lets you declare parameter types, return types, property types, union types, and nullable types, and the moment you do, the language starts catching the same class of boundary bugs the TypeScript track was concerned with: a value of the wrong shape flowing somewhere it shouldn't.
The foundational habit is declare(strict_types=1) and type everything. Without strict types, PHP silently coerces ("5" becomes 5, "abc" might become 0), which is the loose-typing behavior that lets bad data flow undetected — the PHP version of the lying as-cast from the TS track. With strict types declared at the top of every file, type mismatches become errors at the boundary instead of silent coercions deep inside:
php
<?php
declare(strict_types=1);
final class TicketService
{
// Typed parameters and return — the contract is explicit and enforced.
public function assign(TicketId $ticketId, ExpertId $expertId): Ticket
{
// ...
}
}This is the same principle as the foundations' boundary lesson: a typed signature is an enforced contract, where an untyped one is a hope. Typing parameters and returns turns the function's interface into something the language checks, so the structure you designed — these inputs, that output — is verified rather than merely intended. The cost is essentially nothing in modern PHP (the types are declarations, not runtime overhead in the way that matters), and the payoff is that whole categories of "wrong thing passed here" become impossible to run.
Value objects and immutability
The single highest-leverage modeling habit in PHP — and the heart of the DDD approach this track builds toward — is the value object: a small, immutable type that represents a domain concept and carries its own rules, rather than passing raw primitives around. This is drawn directly from Domain-Driven Design in PHP, which treats value objects as the foundational building block, and it's worth understanding why they matter so much architecturally.
Consider an email address. Passed as a raw string, it can be empty, malformed, or duplicated inconsistently across the code, and every function that receives it has to wonder whether it's valid. Modeled as a value object, it validates itself on construction and is guaranteed valid everywhere thereafter:
php
<?php
declare(strict_types=1);
final class EmailAddress
{
public function __construct(public readonly string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: {$value}");
}
}
}
// Now an EmailAddress is valid by construction. Anything typed EmailAddress
// is guaranteed to hold a real email — no downstream function needs to re-check.Two properties make this powerful. First, self-validation: the object cannot exist in an invalid state, because its constructor rejects bad input — which is the parse, don't validate idea (parse untrusted input into a validated type once, at the boundary, rather than re-checking the raw value everywhere downstream). An EmailAddress parameter is then a guarantee, not something each function has to re-verify. Second, immutability: marking properties readonly (PHP 8.1+) means the value can't change after construction, so it can't be mutated into an invalid state somewhere far away, and you can pass it around freely without worrying that someone will alter it. Immutable value objects are safe to share, easy to reason about, and impossible to corrupt after creation. (PHP 8.2's readonly classes make a whole class immutable in one keyword.)
The architectural payoff mirrors the TS track's "make illegal states unrepresentable": a value object pushes correctness into the type, so the validity you want is structural rather than checked-and-rechecked. Money, dates, identifiers, quantities, statuses — the domain is full of concepts that are not really strings and ints, and modeling them as value objects makes the domain logic both safer and more readable, because the types say what they mean.
Avoiding primitive obsession and array-soup
Two specific PHP anti-habits dissolve architecture, and naming them sharpens the discipline above into something you can spot and fix.
Primitive obsession is representing domain concepts with raw primitives — a customer ID as a bare string, money as a float, a status as a magic string "open". The cost is invisible until it bites: nothing stops you passing a ticketId where a customerId was expected (both are strings), nothing stops an invalid status string, and float for money invites rounding bugs that corrupt financial data. The fix is the value objects above — CustomerId, Money, and an enum for status:
php
<?php
declare(strict_types=1);
enum TicketStatus: string
{
case Open = 'open';
case Resolved = 'resolved';
}
// Now a TicketStatus can only ever be one of the defined cases —
// an invalid status string is a type error, not a runtime surprise.PHP 8.1's enums are exactly the tool for the magic-string problem: a finite set of valid values, checked by the type system, replacing error-prone string literals scattered through the code. This is the discriminated-union idea from the TS track in PHP clothing — making the set of legal values explicit and enforced.
Array-soup is PHP's most pervasive structural sin: using associative arrays as ad-hoc data structures, passed between functions, where the array's actual shape lives only in the heads of the developers who wrote the code that reads it. $ticket['status'] works until someone typos the key, or the shape changes, or a key is sometimes missing — and none of it is caught until runtime, often far from the cause. The array is the untyped boundary from the TS track, internal and everywhere. The fix is replacing meaningful arrays with typed objects (value objects, or simple typed classes / DTOs) so the shape is declared and checked. Arrays are fine for genuine collections of like things; they're a liability as a substitute for a typed structure with named, typed fields.
The recurring theme: every primitive or array standing in for a real domain concept is a place the type system can't help you, and converting it to a meaningful type is converting a runtime hope into a compile-time (or static-analysis-time) guarantee.
Enforcing it
Types you declare and habits you intend both erode without enforcement — the governance lesson from Foundations Chapter 6. In PHP, the enforcement tool is static analysis: PHPStan or Psalm, run in CI, analyzing the code for type errors, missing types, and a wide range of bugs without executing it. This is the PHP fitness function for the discipline of this chapter.
What makes static analysis essential in PHP specifically is that PHP's type checking is partly runtime — a type error in an untested code path won't surface until that path runs in production. PHPStan and Psalm move that checking to build time, catching the type mismatch, the possibly-null access, the wrong argument, before it ships — closing the same feedback gap that fitness functions close for architecture. They run at configurable strictness levels, and the high-leverage practice is adopting them at a meaningful level and ratcheting upward over time, treating a clean analysis as a build requirement:
neon
# phpstan.neon — analysis as a build gate
parameters:
level: 8 # high strictness; start lower and ratchet up on legacy code
paths:
- srcWired into CI, static analysis is the fitness function that keeps the type discipline real: the untyped parameter, the primitive that should be a value object, the array access that might be null — flagged at build time, failing the build, the moment they're introduced rather than in a production incident later. The types make your structure explicit; static analysis makes the structure stay explicit.
That's the foundation this track builds on: modern PHP's type system, used fully and enforced by static analysis, makes structure explicit and durable — typed boundaries, self-validating immutable value objects, meaningful types over primitives and arrays — which is exactly the solid ground the domain modeling and Laravel architecture in the coming chapters need.
Practice
Budget about ninety minutes.
Exercise 1: Find the array-soup and primitives (20 min)
In a PHP codebase you work on, find a function that passes around an associative array as an ad-hoc structure, and a domain concept represented as a bare primitive (an ID as a string, money as a float, a status as a magic string). Note for each what bug the lack of a real type invites.
Exercise 2: Build a value object (35 min)
Take one of those primitives and model it as an immutable, self-validating value object (or, for a finite set of values, an enum). Make it readonly, validate in the constructor, and replace a few usages of the raw primitive with it. Note what downstream validation you could delete because the value object guarantees validity.
Exercise 3: Turn on the analyzer (35 min)
If your project doesn't run PHPStan or Psalm, add one at a modest level and look at what it flags — each finding is a latent type bug. If it already runs, raise the level by one and see what surfaces. Pick one real finding and fix it, and note whether it was a bug that could have shipped to production.
Deliverable
A short before/after: one domain primitive or array converted to a typed value object/enum (with self-validation), and a note on your project's static-analysis status (level, biggest category of findings) — with a sentence on the class of bug each change now prevents at build time.
Check yourself
- Explain why
declare(strict_types=1)and full type declarations are an architectural choice in PHP. - Define a value object and explain self-validation and immutability (
readonly) and why each matters. - Identify primitive obsession and array-soup, and give the typed alternative for each.
- Explain why static analysis (PHPStan/Psalm) is the fitness function for PHP type discipline.
What a strong answer demonstrates: you treat typing as a structural choice, not a style preference — you can say why strict_types turns a hoped-for contract into an enforced one, and why a self-validating readonly value object makes an illegal state unrepresentable rather than merely discouraged. The strongest answers tie all four points to one thread: each habit moves a guarantee from "checked at runtime, somewhere, maybe" to "enforced at the boundary or at build time," with static analysis as the fitness function that keeps it from eroding.
Going deeper (optional)
- Domain-Driven Design in PHP (Buenosvinos & Soronellas) — the chapters on value objects and entities are the direct source for the value-object modeling here, with more depth on equality, immutability, and domain modeling.
- Primary source: Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003) — the original treatment of value objects (and the entity/value distinction) that the PHP book builds on; read the building-blocks chapter for why a self-validating immutable value is a modeling tool, not just a class.
- Primary source: Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) — the canonical catalog behind "a primitive standing in for a concept is where the type system can't help you"; its Value Object and Money patterns are the seminal version of the modeling this chapter advocates.
- The PHPStan and Psalm documentation for static-analysis configuration and levels. (Flagged: PHP 8.x specifics — readonly, enums, the latest type features — and static-analysis tooling are current-language/ecosystem knowledge; the DDD book covers the modeling principles, and Laravel/PHP docs cover the language specifics.)
Key takeaways
- Modern PHP has a real type system; using it fully is an architectural choice —
declare(strict_types=1)and typed signatures turn intentions into enforced contracts. - Value objects — small, immutable (
readonly), self-validating types — model domain concepts so they're valid by construction, the PHP form of "make illegal states unrepresentable." - Avoid primitive obsession (use value objects and enums over bare strings/ints) and array-soup (use typed objects over ad-hoc arrays) — each primitive or array standing in for a concept is where the type system can't help you.
- Static analysis (PHPStan/Psalm) in CI is the fitness function that keeps the type discipline honest, catching at build time what PHP would otherwise only fail on at runtime.
Next: Laravel Architecture Boundaries — separating controllers, services, and domain layers so a Laravel app has the clean internal structure the foundations call for.