Most of a project’s life is maintenance: fixing bugs, adding features, upgrading. Code that is easy to change saves more time than code that is clever or fast.
Principles that matter
Single responsibility. A class or function has one reason to change. A class that validates, saves, emails and renders will change for four different reasons.
Don’t repeat yourself. One rule, one place (see Avoiding Duplicate Code). But do not merge things that only look alike.
Depend on abstractions. Ask for an interface, not a concrete class (see Dependency Injection). Swapping an implementation then does not ripple through the code.
Keep it simple. The simplest thing that works, and no speculative features.
Make illegal states impossible. Use types, enums and value objects so invalid data cannot exist.
Code smells
A smell is a sign that code may need work.
| Smell | What it looks like | Typical cure |
|---|---|---|
| Long function | Scrolls over a screen | Extract smaller functions |
| Long parameter list | 6+ parameters | Group them into an object |
| Deep nesting | if inside if inside if | Early returns, extract |
| Duplicate code | Copy-pasted blocks | Extract a function or class |
| God class | One class that does everything | Split by responsibility |
| Magic values | if ($status == 3) | Constants or enums |
| Boolean flags | save($x, true, false) | Named arguments, separate methods |
| Global state | global $db, static properties | Pass dependencies in |
| Primitive obsession | Strings and arrays everywhere | Value objects (Email, Money) |
Example: replace a flag with clear methods
<?php
// Unclear: what do the booleans mean?
function renderBad(string $text, bool $escape, bool $newlines): string
{
$text = $escape ? htmlspecialchars($text) : $text;
return $newlines ? nl2br($text) : $text;
}
// Clear
final class Html
{
public static function escaped(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}
public static function paragraphs(string $text): string
{
return nl2br(self::escaped($text), false);
}
}
echo renderBad("a<b", true, false) . " | " . Html::paragraphs("line 1\nline 2");
a<b | line 1<br>
line 2
Example: a value object instead of a loose string
<?php
final class Email
{
private function __construct(public readonly string $value) {}
public static function from(string $raw): self
{
$email = strtolower(trim($raw));
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException("Invalid email: $raw");
}
return new self($email);
}
}
function sendWelcome(Email $to): string
{
return "Welcome email to " . $to->value; // no need to re-check the address
}
echo sendWelcome(Email::from(" Isha@Example.COM "));
Welcome email to isha@example.com
Once an Email exists, everyone can trust it. The checking happens in one place.
Refactoring safely
Refactoring means changing the structure of code without changing its behaviour. Do it in small steps:
- Make sure there is a test (or at least a manual check) for the behaviour.
- Make one small change (rename, extract, move).
- Run the test. Commit.
- Repeat.
Never mix a refactor with a new feature in one change. If something breaks, you will not know which one caused it.
Tests make change safe
Automated tests let you change code with confidence. Start with tests for the important business rules (prices, permissions, validation), not for every getter.
Document the important things
- A
READMEthat explains how to install, configure and run the project. - Why comments for surprising decisions.
- Type declarations and DocBlocks for what the types cannot say (
@param string[] $tags). - A short architecture note: the folder layout and the request flow (see Clean PHP Project Structure).
Version control habits
- Small commits with clear messages (“Validate email on signup”).
- One purpose per pull request.
- Review each other’s code. A fresh reader finds confusing parts.
Handle upgrades early
Keep PHP, Composer packages and frameworks updated in small steps. Fix deprecation notices when they appear (see PHP Errors). A project that is five versions behind is expensive to rescue.
The final test
Could a new teammate:
- Install and run the project from the README?
- Find where a feature lives within a few minutes?
- Change it without fear of breaking something unrelated?
If yes, the code is maintainable.
You are ready for real projects
Exercise: find the smells
This function has several problems. Name three, and say how you would fix each: function process($d, $t, $f) { $r = ...; if ($f == 1) { if ($t == 'a') { /* 80 lines */ } } return $r; }
Show the answer
- Unclear names (
$d,$t,$f,process): rename to say what they are. - Magic values and a flag (
$f == 1,$t == 'a'): use named constants or an enum and separate methods. - Deep nesting and a very long block: use early returns and extract the 80 lines into smaller, named functions.
Also: no types, and loose == comparison.