Originally published on decentphp.dev — part of the Writing decent PHP course. View this lesson on the original site.

Putting the chapter into practice on one small file

This chapter has covered a lot of ground: naming by intent, units in variables, verb prefixes on booleans and functions, collection names, class names, file layout, domain language, utility language, abbreviations, drift, safe renaming, and review.

It is one thing to know the rules. It is another to apply them to a real piece of code with ten smells at once.

This lesson is a walkthrough of exactly that: a medium-sized class with every Chapter 2 smell you can cram into 40 lines, renamed in small passes until it reads honestly.

Each pass targets one lesson's worth of ideas. Do the passes in order — trying to fix everything at once is exactly how renames turn into 400-line PRs nobody wants to review (lesson 2.12).

The starting point

We've inherited this file from a previous developer. It "works". The tests are green. Nobody has opened it in four months.

<?php

namespace App\Helpers;

final class UserMgr
{
    public function __construct(
        private $db,
        private $mailer
    ) {}

    public function doStuff(array $data): array
    {
        $res = [];
        $lst = $this->db->query("SELECT * FROM users WHERE status = 'a'");

        foreach ($lst as $item) {
            $t = $item['amt'] * 1.2;

            if ($t > 100 && $item['vf'] == 1 && $item['stat'] !== 'b') {
                $item['total'] = $t;
                $item['processed'] = true;
                $this->db->update('users', $item);
                $this->mailer->send($item['eml'], 'Your total: ' . $t);
                $res[] = $item;
            }
        }

        return $res;
    }

    public function validateUser($u): bool
    {
        $x = $this->db->query("SELECT * FROM users WHERE id = " . $u['id']);

        if (count($x) > 0) {
            $this->db->update('users', ['id' => $u['id'], 'last_checked' => time()]);
            return true;
        }

        return false;
    }
}

A reader trying to land in this file has to answer all of these before they can change anything:

  • what domain does UserMgr belong to?
  • what does doStuff actually do?
  • what is $t? what is 1.2? what unit is $item['amt']?
  • what does vf stand for? stat? eml?
  • why does validateUser mutate the database?
  • what Helpers namespace does this even belong to?

Every one of those questions is a naming smell from earlier in this chapter.

Pass 1 — Fix the names that lie (lesson 2.11)

Before we tidy anything cosmetic, we fix the names that are actively wrong. validateUser is the obvious one: it doesn't validate, it checks existence and also writes to the database.

<?php

namespace App\Helpers;

final class UserMgr
{
    public function doStuff(array $data): array
    {
        // unchanged
    }

    public function findUserAndMarkChecked(int $userId): bool
    {
        $result = $this->db->query("SELECT * FROM users WHERE id = " . $userId);

        if (count($result) > 0) {
            $this->db->update('users', ['id' => $userId, 'last_checked' => time()]);
            return true;
        }

        return false;
    }
}

The name now warns every caller: this reads and mutates. That exposes the real problem: it probably shouldn't do both. We leave it honestly named for now and come back to the split in a later pass.

We also notice the SQL is string-concatenating an integer — that's a security smell (Chapter 14 territory). We'll leave a comment for a follow-up PR. Chapter 2 is about names; one refactor at a time (lesson 2.12).

Pass 2 — Give every variable an intent (lessons 2.1, 2.2)

Now the quick wins: every one-letter or abbreviated variable becomes a name that tells you what it holds and, where relevant, what unit it's in.

public function doStuff(array $data): array
{
    $qualifyingUsers = [];
    $activeUsers = $this->db->query("SELECT * FROM users WHERE status = 'a'");

    foreach ($activeUsers as $activeUser) {
        $totalInPence = (int) round($activeUser['amount_in_pence'] * 1.2);

        if (
            $totalInPence > 10000
            && $activeUser['vip_flag'] == 1
            && $activeUser['status'] !== 'blocked'
        ) {
            $activeUser['total_in_pence'] = $totalInPence;
            $activeUser['processed'] = true;
            $this->db->update('users', $activeUser);
            $this->mailer->send($activeUser['email'], 'Your total: ' . $totalInPence);
            $qualifyingUsers[] = $activeUser;
        }
    }

    return $qualifyingUsers;
}

Three things happened:

  • every variable now tells you what, not how it's typed ($lst -> $activeUsers, $t -> $totalInPence)
  • units are now part of names (_in_pence) — no more guessing if 100 means pounds or pence
  • the loop variable is singular ($activeUser from $activeUsers), per lesson 2.5

We kept 1.2 as a literal. It still smells. Next pass.

Pass 3 — Name the booleans and the constants (lessons 2.3, 2.9)

The magic number 1.2 becomes a named constant. The boolean-shaped conditions become named predicates the eye can skim.

private const VAT_MULTIPLIER = 1.2;
private const HIGH_VALUE_THRESHOLD_IN_PENCE = 10000;

public function doStuff(array $data): array
{
    $qualifyingUsers = [];
    $activeUsers = $this->db->query("SELECT * FROM users WHERE status = 'a'");

    foreach ($activeUsers as $activeUser) {
        $totalInPence = (int) round($activeUser['amount_in_pence'] * self::VAT_MULTIPLIER);

        $isHighValue = $totalInPence > self::HIGH_VALUE_THRESHOLD_IN_PENCE;
        $isVip = $activeUser['vip_flag'] == 1;
        $isBlocked = $activeUser['status'] === 'blocked';

        if ($isHighValue && $isVip && !$isBlocked) {
            // mutation + email + collection append
        }
    }

    return $qualifyingUsers;
}

The if statement now reads in plain English: if this user is high-value and a VIP and not blocked. A reviewer reading the diff can verify the business rule without parsing three conditionals.

Each condition is named with a verb prefix (is...). Each magic number is named at the level of the rule it encodes, not the raw value.

Pass 4 — Give the function an honest name (lesson 2.4)

doStuff now tells every reader nothing. We can name it based on what it actually does:

  • it reads active users
  • filters to high-value VIPs who aren't blocked
  • charges VAT
  • writes a total and processed = true
  • emails them
  • returns the list

That's not one function's job, but let's name it honestly first (lesson 2.11) before we split:

public function chargeVatAndEmailHighValueVips(): array

Too long? Not really — the length is telling you the function does too much. That's now visible. We commit this as a rename-only PR (lesson 2.12) and deal with the split in a follow-up.

We also drop the unused $data parameter now that we're here — the function never uses it. The old signature was a promise it never kept.

Pass 5 — Fix the class and the namespace (lessons 2.6, 2.7, 2.8, 2.9, 2.10)

UserMgr in App\Helpers violates every folder and class lesson in the chapter:

  • Mgr is an abbreviation nobody says aloud (2.10)
  • Manager is a vague class suffix (2.6)
  • Helpers is a utility namespace placeholder (2.9)
  • the file isn't grouped with related billing code (2.7)
  • the domain doesn't match — this is billing, not user management (2.8)

The final shape:

<?php

namespace App\Billing;

final class HighValueVipCharger
{
    private const VAT_MULTIPLIER = 1.2;
    private const HIGH_VALUE_THRESHOLD_IN_PENCE = 10000;

    public function __construct(
        private UserRepository $userRepository,
        private Mailer $mailer
    ) {}

    public function chargeVatAndEmailHighValueVips(): array
    {
        $qualifyingUsers = [];

        foreach ($this->userRepository->findActiveUsers() as $activeUser) {
            if (!$this->isEligibleForHighValueCharge($activeUser)) {
                continue;
            }

            $totalInPence = $this->calculateTotalWithVatInPence($activeUser['amount_in_pence']);

            $this->userRepository->recordCharge($activeUser['id'], $totalInPence);
            $this->mailer->send($activeUser['email'], 'Your total: ' . $totalInPence);

            $qualifyingUsers[] = $activeUser;
        }

        return $qualifyingUsers;
    }

    private function isEligibleForHighValueCharge(array $user): bool
    {
        $projectedTotal = $this->calculateTotalWithVatInPence($user['amount_in_pence']);

        return $projectedTotal > self::HIGH_VALUE_THRESHOLD_IN_PENCE
            && $user['vip_flag'] === 1
            && $user['status'] !== 'blocked';
    }

    private function calculateTotalWithVatInPence(int $amountInPence): int
    {
        return (int) round($amountInPence * self::VAT_MULTIPLIER);
    }
}

The class name now tells you exactly what it does. The namespace (App\Billing) matches the domain. The eligibility rule lives in its own named predicate. VAT calculation is one small function. The SQL has been pushed behind a UserRepository with real method names, not ad-hoc query strings.

The order of the passes matters

Each pass was small, reviewable on its own, and built on the last:

  1. fix names that lie
  2. name variables by intent and unit
  3. name booleans and extract magic numbers
  4. name the function honestly (even if that reveals it does too much)
  5. name the class, namespace, and file location

Doing these in order keeps each PR reviewable and each rename safe. Trying to do them all at once is the recipe for the "Rename validateUser, fix race condition, tidy logging" PR from lesson 2.12 — impossible to review, impossible to bisect, impossible to revert cleanly.

What you just did, zoomed out

Looking at the first and last versions side by side, the actual business logic barely changed. We didn't add features. We didn't optimise performance. We didn't restructure the data.

We just made the code tell the truth about itself.

That is what the whole chapter has been about. Good names are not decoration — they are the running documentation your team reads every time they open a file. When the names are honest, every other chapter in this course gets easier: refactoring (Chapter 4) is safer because you can see what depends on what, testing (Chapter 8) is simpler because inputs and outputs are named, and SOLID (Chapter 7) makes sense because responsibilities have visible, speakable names.

The next lesson gives you three bad code samples to try this on yourself, plus a link to the reference implementations.

From Writing decent PHP — Naming variables, functions, classes, and files well