Skip to content

Clean Code Principles

Write code that is readable, maintainable, and minimal. Favor clarity over cleverness.

KISS (Keep It Simple, Stupid)

Choose the simplest solution that works. Complexity should be justified by actual requirements, not anticipated ones.

Guidelines:

  • One level of abstraction per method
  • No premature optimization
  • Avoid over-engineering

Anti-patterns to avoid:

Anti-pattern Problem
Generic factory for single implementation Unnecessary indirection
Abstract base class for single concrete class Premature abstraction
Configuration file for hardcoded values Over-engineering
Strategy pattern for one algorithm Speculative design

DRY (Don't Repeat Yourself)

Extract repeated logic, but recognize that duplication is better than the wrong abstraction.

Guidelines:

  • Wait for 3 occurrences before abstracting (Rule of Three)
  • Similar code is not always duplicate code
  • Different reasons to change = not duplication

When NOT to DRY:

  • Two handlers with similar but different business rules (they evolve independently)
  • Test setup that looks similar but tests different scenarios
  • Coincidental similarity (same code today, different purposes)

Example of acceptable duplication:

// These look similar but serve different business purposes
// Extracting would create coupling between unrelated features

// In CreateBookHandler
$this->repository->save($book);
$this->eventBus->dispatch(new BookCreatedEvent(...));

// In UpdateBookHandler
$this->repository->save($book);
$this->eventBus->dispatch(new BookUpdatedEvent(...));

YAGNI (You Aren't Gonna Need It)

Build only what is needed now. Future requirements are uncertain; today's code is certain.

Guidelines:

  • No speculative features
  • No "might be useful later" code
  • Solve today's problem, not tomorrow's

Violations:

Violation Better approach
Configuration for single value Hardcode it, extract when needed
Interface for single implementation Use concrete class, extract interface when second implementation appears
Generic solution for specific problem Solve the specific problem

Naming Conventions

Names should reveal intent. A reader should understand purpose without reading implementation.

Conventions:

Element Style Example
Classes PascalCase, noun CreateBookHandler, BookRepository
Methods camelCase, verb validateIsbn(), calculateTotal()
Variables camelCase, descriptive $bookTitle, $loanPeriod, $isAvailable
Constants UPPER_SNAKE_CASE MAX_LOAN_DAYS, DEFAULT_STATUS

Names to avoid:

  • Generic: Manager, Helper, Utils, Data, Info, Processor
  • Abbreviated: $temp, $val, $str, $arr
  • Meaningless: $data, $result, $item, $stuff

Method Guidelines

Methods should be small, focused, and operate at a single level of abstraction.

Guidelines:

  • Short methods (< 20 lines ideal)
  • Few parameters (< 4 ideal)
  • Single level of abstraction
  • Early returns for guard clauses
  • No boolean flags that change behavior

Example of early return:

public function borrow(): self
{
    if ($this->status !== BookStatus::Available) {
        throw BookNotAvailableException::withId($this->id);
    }

    return $this->withStatus(BookStatus::Borrowed);
}

Class Guidelines

Classes should be small, focused, and cohesive.

Guidelines:

  • Small, focused classes with single responsibility
  • Cohesive methods (most methods use most instance variables)
  • Composition over inheritance
  • No god classes (classes that know too much or do too much)

Warning signs of a god class:

  • More than ~200 lines
  • Many unrelated methods
  • Methods that don't use instance variables
  • Name contains "Manager", "Controller", "Handler" for non-architectural classes

Comments

Code should be self-documenting. Comments explain "why", not "what".

Guidelines:

  • Comment only non-obvious business rules
  • Delete commented-out code (version control preserves history)
  • No PHPDoc for self-evident types (see coding standards)
  • Prefer renaming over commenting

Good comment:

// ISBN-10 uses modulo 11 with X representing 10
if ($checkDigit === 10) {
    return 'X';
}

Bad comment:

// Get the book
$book = $this->repository->findById($id);

Error Handling

Fail fast with specific, informative exceptions.

Guidelines:

  • Use specific domain exceptions
  • Fail fast with clear messages
  • Don't catch exceptions just to rethrow unchanged
  • Domain exceptions for business rule violations

Exception hierarchy:

DomainException
├── BookNotFoundException
├── InvalidIsbnException
├── BookNotAvailableException
└── ...

Law of Demeter

A method should only call methods on its immediate collaborators, not on objects returned by those collaborators. This reduces coupling between classes.

Principle: "Talk only to your friends"

Violation:

// Bad - reaching through multiple objects
$city = $order->getCustomer()->getAddress()->getCity();

Better approaches:

// Option 1: Delegate to the object
$city = $order->getShippingCity();

// Option 2: Pass what you need directly
public function __construct(
    private readonly string $shippingCity,
) {}

Allowed calls:

  • Methods on $this
  • Methods on objects passed as parameters
  • Methods on objects created within the method
  • Methods on direct dependencies (injected via constructor)

Command-Query Separation (CQS)

A method should either change state (command) or return data (query), never both. This principle is the foundation of CQRS at the method level.

Commands:

  • Modify state
  • Return void
  • Named with verbs: save(), delete(), borrow()

Queries:

  • Return data
  • No side effects
  • Named descriptively: findById(), isAvailable(), count()

Violation:

// Bad - modifies state AND returns data
public function borrowAndGetDueDate(): DateTimeImmutable
{
    $this->status = BookStatus::Borrowed;
    return $this->calculateDueDate();
}

Better:

// Separate command and query
public function borrow(): self
{
    return $this->withStatus(BookStatus::Borrowed);
}

public function dueDate(): DateTimeImmutable
{
    return $this->calculateDueDate();
}

Avoid Primitive Obsession

Use Value Objects instead of primitive types for domain concepts. This centralizes validation and makes code more expressive.

Primitive obsession:

// Bad - primitives everywhere
public function createUser(
    string $email,
    string $password,
    string $phone,
): User

Value Objects:

// Good - domain concepts are explicit
public function createUser(
    Email $email,
    Password $password,
    PhoneNumber $phone,
): User

Benefits:

Aspect Primitive Value Object
Validation Scattered across codebase Centralized in constructor
Type safety Any string accepted Only valid values accepted
Documentation Requires comments Self-documenting
Refactoring Find all usages manually Type system helps

When to create a Value Object:

  • The value has validation rules
  • The value has behavior (formatting, comparison)
  • The value appears in multiple places
  • The primitive could be confused with another (two string parameters)

See Also