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:
Bad comment:
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:
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
stringparameters)
See Also
- Clean Code PHP (GitHub)
- Value Objects - Primitive obsession solution
- CQRS - Command-Query Separation at architectural level