Everyday PHP syntax
Modern typed syntax used in the examples. Add declare(strict_types=1); at the top of PHP files when you want scalar types checked strictly at call sites.
PHP syntax · typesTypes and nullable values
Type inputs and outputs to make a function's contract clear. Nullable types use ?; union types accept one of several declared types.
function displayName(?string $name): string
{
return $name ?? 'Guest';
}
function total(float|int $price, int $quantity): float
{
return $price * $quantity;
}
Use it when: a function or method has a clear expected input and output. Prefer a specific type over mixed when practical.
PHP syntax · classesConstructor property promotion
Declare and initialise properties directly in the constructor argument list. This is useful for simple dependency or value-object fields.
final class UserNotifier
{
public function __construct(
private Mailer $mailer,
private string $fromAddress,
) {}
}
$notifier = new UserNotifier($mailer, 'help@example.com');
Use it when: constructor arguments map directly to properties without extra setup logic.
PHP syntax · iterationArrays and foreach
Associative arrays preserve useful names; foreach iterates values or key/value pairs. Destructuring assigns known positions to variables.
$users = ['Ada' => 1, 'Linus' => 2];
foreach ($users as $name => $id) {
echo "$name: $id\n";
}
[$status, $body] = [200, 'OK'];
Use it when: you have a small ordered or named collection. Use a class when the data needs behaviour or stronger invariants.
PHP syntax · branchingMatch expressions
match returns a value, compares strictly and does not fall through. Multiple values can share one result.
$label = match ($status) {
200 => 'success',
404 => 'not found',
500, 503 => 'server error',
default => 'unknown',
};
Use it when: one expression needs to map to a result based on a small set of exact values.
PHP syntax · errorsExceptions and cleanup
Catch a specific exception where the application can recover, translate it into a suitable response, or add useful context. A finally block runs whether or not an exception was thrown.
try {
$invoice = $service->find($id);
} catch (InvoiceNotFound $exception) {
throw new RuntimeException('Could not load the invoice.', 0, $exception);
} finally {
$timer->stop();
}
Use it when: an exceptional condition needs handling at this layer. Let unexpected exceptions bubble to the application's error handler.
PHP syntax · functionsClosures and arrow functions
Arrow functions are concise for one expression and capture outer variables by value automatically. Closures support multiple statements and explicit capture rules.
$taxRate = 0.20;
$withTax = fn (float $amount): float =>
$amount * (1 + $taxRate);
$active = array_filter(
$users,
fn (User $user): bool => $user->isActive(),
);
Use it when: passing a small piece of behaviour to a function such as array_map, a collection or a callback.
PHP syntax · valuesEnums
Enums represent a fixed set of possible values. Backed enums also map each case to a string or integer for storage and APIs.
enum DeliveryStatus: string
{
case Pending = 'pending';
case Sent = 'sent';
case Failed = 'failed';
}
$status = DeliveryStatus::Pending;
echo $status->value; // 'pending'
Use it when: a value must be one of a known set and named cases are clearer than free-form strings.
PHP syntax · objectsObjects and references
Objects are passed as object handles by value: mutating the object is visible to the caller, but reassigning the parameter does not replace the caller's variable. Avoid & unless reference mutation is deliberate.
function updateDisplayName(User $user): void
{
$user->name = 'Ada'; // caller sees this change
$user = new User(); // caller still has old object
}
Remember: a parameter type such as User does not mean the argument variable itself is passed by reference.
SOLID principles
Five design guidelines for keeping changes local and implementations interchangeable. Each example shows the principle in code; they are guidance, not a requirement to add abstractions to every small task.
SRP · Single responsibilityGive each component one reason to change
Keep calculation separate from delivery. A change to invoice arithmetic should not require editing email sending, and a change to email transport should not affect arithmetic.
final class InvoiceCalculator
{
/** @param list<array{quantity: int, unitPriceCents: int}> $lines */
public function totalCents(array $lines): int
{
return array_sum(array_map(
fn (array $line): int => $line['quantity'] * $line['unitPriceCents'],
$lines,
));
}
}
final class InvoiceEmailer
{
public function __construct(private Mailer $mailer) {}
public function send(string $recipient, string $message): void
{
$this->mailer->send($recipient, $message);
}
}
Use it when: one class is changing for unrelated reasons, such as business rules and infrastructure concerns being mixed together.
OCP · Open / closedAdd new behaviour behind a stable contract
Area calculation works with the Shape contract. A new shape supplies its own area method; the calculator does not need a new type check.
interface Shape
{
public function area(): float;
}
final readonly class Circle implements Shape
{
public function __construct(public float $radius)
{
if ($radius < 0) {
throw new InvalidArgumentException('Radius cannot be negative.');
}
}
public function area(): float
{
return pi() * $this->radius ** 2;
}
}
final class Rectangle implements Shape
{
public function __construct(
private float $width,
private float $height,
) {
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException('Dimensions cannot be negative.');
}
}
public function area(): float
{
return $this->width * $this->height;
}
}
final class AreaCalculator
{
/** @param iterable<Shape> $shapes */
public function total(iterable $shapes): float
{
$total = 0.0;
foreach ($shapes as $shape) {
$total += $shape->area();
}
return $total;
}
}
Use it when: new variants are likely and the existing caller can work through a stable interface.
Open / closed does not mean never edit existing code. It means likely extensions should not force repetitive changes to stable callers.
LSP · Liskov substitutionMake every implementation honour the contract
For a non-negative subtotal, both implementations return a discount between zero and the subtotal. The caller can use either through the same interface without checking the concrete class.
interface Discount
{
/** @param non-negative-int $subtotalCents */
public function amountFor(int $subtotalCents): int;
}
final class NoDiscount implements Discount
{
public function amountFor(int $subtotalCents): int
{
return 0;
}
}
final class PercentageDiscount implements Discount
{
public function __construct(private int $percentage)
{
if ($percentage < 0 || $percentage > 100) {
throw new InvalidArgumentException('Percentage must be 0–100.');
}
}
public function amountFor(int $subtotalCents): int
{
return (int) round($subtotalCents * $this->percentage / 100);
}
}
function totalAfterDiscount(int $subtotalCents, Discount $discount): int
{
return $subtotalCents - $discount->amountFor($subtotalCents);
}
Use it when: a caller accepts an interface or parent type. Every implementation must preserve its promises, including valid inputs, output ranges and expected behaviour.
ISP · Interface segregationKeep interfaces focused on what callers use
Separate print and scan capabilities so a basic printer does not need to implement a meaningless scan method.
interface Printer
{
public function print(string $document): void;
}
interface Scanner
{
public function scan(): string;
}
final class BasicPrinter implements Printer
{
public function print(string $document): void
{
echo "[print] $document\n";
}
}
final class OfficeMachine implements Printer, Scanner
{
public function print(string $document): void
{
echo "[print] $document\n";
}
public function scan(): string
{
return 'scanned document';
}
}
Use it when: clients or implementations are forced to depend on methods they do not need.
DIP · Dependency inversionKeep business rules independent of infrastructure
The order workflow depends on a storage contract and a receipt-sending contract. Concrete database and email implementations are supplied when the application assembles the service.
interface OrderRepository
{
public function save(Order $order): void;
}
interface ReceiptSender
{
public function send(Order $order): void;
}
final class PlaceOrder
{
public function __construct(
private OrderRepository $orders,
private ReceiptSender $receipts,
) {}
public function handle(Order $order): void
{
$order->place();
$this->orders->save($order);
$this->receipts->send($order);
}
}
Use it when: high-level business code should not be tied to a database, framework, filesystem, or external service implementation.
Patterns
A selection of creational, structural and behavioural patterns, with guidance on when each is useful.
Quick guideChoosing between similar patterns
These patterns can look alike at first. Identify what is changing and how control should flow.
| Compare | Choose this when… |
|---|
| Static factory / Factory Method | A static factory is a named method that centralises object creation. Factory Method lets a creator subclass choose the product used by a shared workflow. |
| Adapter / Decorator | An Adapter changes an object’s interface to fit a caller. A Decorator keeps the same interface and adds behaviour around it. |
| Observer / Chain of responsibility | Observer notifies multiple listeners about an event. A chain passes one request from handler to handler, and a handler may stop it. |
| Strategy / Chain of responsibility | Strategy selects one interchangeable algorithm. A chain gives multiple handlers a chance to process the same request. |
Pattern · DIPDependency injection
Provide a class with the collaborators it needs rather than letting it create them. Constructor injection makes required dependencies clear and replaceable in tests.
interface UserRepository
{
public function find(int $id): ?User;
}
final class UserChecker
{
public function __construct(private UserRepository $users) {}
public function isEnabled(int $id): bool
{
return $this->users->find($id)?->isEnabled() ?? false;
}
}
$checker = new UserChecker($repository);
Use it when: a class depends on a database, API, clock, mailer or other replaceable service.
Dependency injection is a technique; a container is one way to assemble objects. Avoid injecting the container into every class (service locator).
Pattern · creationalStatic factory
A named creation method chooses and returns an object. It can clarify construction when a constructor alone does not explain the choice.
interface Exporter
{
public function export(array $rows): string;
}
final class Exporters
{
public static function for(string $format): Exporter
{
return match ($format) {
'json' => new JsonExporter(),
'csv' => new CsvExporter(),
default => throw new InvalidArgumentException(
"Unknown format: $format"
),
};
}
}
$exporter = Exporters::for('json');
Use it when: named creation improves readability or hides construction details.
Watch for: static factories can make implementations harder to substitute. Inject a factory contract if callers need varied creation policies.
Pattern · structuralAdapter
Wrap an existing class and translate its interface into the one the rest of your code expects.
interface Book
{
public function open(): void;
public function nextPage(): int;
}
final class KindleAdapter implements Book
{
public function __construct(private Kindle $kindle) {}
public function open(): void
{
$this->kindle->unlock();
}
public function nextPage(): int
{
$this->kindle->pressNext();
return $this->kindle->currentPage();
}
}
Use it when: integrating a legacy class, third-party SDK or API with an incompatible interface.
Pattern · structuralDecorator
Wrap an object with another object that implements the same interface, adding behaviour while keeping wrappers stackable.
interface Booking
{
public function price(): int;
}
final class Room implements Booking
{
public function price(): int { return 80; }
}
final class Breakfast implements Booking
{
public function __construct(private Booking $booking) {}
public function price(): int { return $this->booking->price() + 15; }
}
$booking = new Breakfast(new Room()); // 95
Use it when: optional features should combine in different ways without a subclass for each combination.
Pattern · behaviouralObserver (publish / subscribe)
A publisher announces an event to registered listeners without knowing what each listener does.
interface Listener
{
public function handle(UserRegistered $event): void;
}
final class RegistrationPublisher
{
/** @param list<Listener> $listeners */
public function __construct(private array $listeners) {}
public function publish(UserRegistered $event): void
{
foreach ($this->listeners as $listener) {
$listener->handle($event);
}
}
}
Use it when: several independent reactions follow one event, such as a welcome email and an audit record.
Watch for: hidden listeners can make control flow hard to trace. Use explicit calls for one simple next step.
Pattern · behaviouralChain of responsibility
Pass a request through handlers in order. Each can act, pass it on or stop the chain.
interface Check
{
public function handle(Request $request, Closure $next): Response;
}
final class RequireVerifiedUser implements Check
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user?->isVerified()) {
return new Response(403);
}
return $next($request);
}
}
// Pipeline: Authenticate → RequireVerifiedUser → controller
Use it when: a variable sequence of checks or handlers should be composed, as in middleware, validation or approval flows.
Watch for: a handler may always pass the request on, or stop after handling or rejecting it. Make continuation explicit and avoid hard-coding the next concrete handler.
Pattern · business ruleSpecification
Represent a rule as an object with a clear yes/no question. Named rules can be tested, reused and sometimes combined.
interface Specification
{
public function isSatisfiedBy(Product $product): bool;
}
final class InStock implements Specification
{
public function isSatisfiedBy(Product $product): bool
{
return $product->stockCount() > 0;
}
}
$available = (new InStock())->isSatisfiedBy($product);
Use it when: a business rule has a name, is reused or needs combining with other rules.
For a one-off condition, a plain if is clearer. Type the input rather than accepting mixed when possible.
Common addition · behaviouralStrategy
Put interchangeable algorithms behind one interface and choose the implementation at runtime.
interface ShippingCost
{
public function calculate(Order $order): int;
}
final class StandardShipping implements ShippingCost
{
public function calculate(Order $order): int { return 500; }
}
final class Checkout
{
public function __construct(private ShippingCost $shipping) {}
public function total(Order $order): int
{
return $order->subtotal() + $this->shipping->calculate($order);
}
}
$checkout = new Checkout(new StandardShipping());
Use it when: a choice among algorithms is growing into a conditional or must vary by configuration.
Common addition · creationalFactory method
A creator method chooses which product implementation to build. Unlike a static factory, subclasses override the creation method.
interface Notification
{
public function send(string $message): void;
}
abstract class Notifier
{
abstract protected function makeNotification(): Notification;
public function notify(string $message): void
{
$this->makeNotification()->send($message);
}
}
final class EmailNotifier extends Notifier
{
protected function makeNotification(): Notification
{
return new EmailNotification();
}
}
Use it when: subclasses vary the product used in a shared workflow. A small injected factory may be simpler for a simple choice.
Common addition · creationalBuilder
Construct a complex object step by step, especially when it has optional settings or rules to validate. The builder returns a separate product object.
final readonly class ReportQuery
{
public function __construct(
public array $filters,
public ?string $sort,
) {}
}
final class ReportQueryBuilder
{
private array $filters = [];
private ?string $sort = null;
public function where(string $field, string $value): self
{
$this->filters[$field] = $value;
return $this;
}
public function sortBy(string $field): self
{
$this->sort = $field;
return $this;
}
public function build(): ReportQuery
{
return new ReportQuery($this->filters, $this->sort);
}
}
$query = (new ReportQueryBuilder())
->where('status', 'open')
->sortBy('created_at')
->build();
Use it when: construction has meaningful steps, optional options or invariants to validate.
For a small value object, named constructor arguments may be enough.
Common addition · behaviouralCommand
Represent an action as an object. A command bus can route it to a handler, making the action easy to log, queue or retry.
final readonly class SendInvoice
{
public function __construct(public int $invoiceId) {}
}
interface InvoiceRepository
{
public function find(int $id): Invoice;
}
final class SendInvoiceHandler
{
public function __construct(
private InvoiceRepository $invoices,
private Mailer $mailer,
) {}
public function handle(SendInvoice $command): void
{
$invoice = $this->invoices->find($command->invoiceId);
$this->mailer->send($invoice->email());
}
}
$commandBus->dispatch(new SendInvoice($invoiceId));
Use it when: an action should be represented as data so a bus or queue can record, route, delay or retry it. Keep service dependencies in the handler, not in a serialized queued command.
Common addition · data accessRepository
Give domain code a collection-like way to load and save objects while hiding persistence details behind a contract.
interface OrderRepository
{
public function find(OrderId $id): ?Order;
public function save(Order $order): void;
}
final class PlaceOrder
{
public function __construct(private OrderRepository $orders) {}
public function handle(Order $order): void
{
$order->place();
$this->orders->save($order);
}
}
Use it when: persistence details leak into business rules or the domain needs a meaningful storage boundary.
Watch for: wrapping every ORM call in a pass-through repository adds ceremony without creating a useful boundary.