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

Names become lies the moment behaviour drifts

Every name you write is a promise. validateUser() promises to validate a user. getOrder() promises to fetch an order. sendEmail() promises to send an email.

Code changes all the time. Names rarely change with it.

A function starts life called validateUser(). Six months later it also saves the user, fires an event, and writes an audit log. The name still says "validate". Every reader who lands on that function now learns something painful: the name lies. And once readers learn a name lies, they stop trusting any name in the codebase.

This lesson is about catching that drift early, and treating renaming as a normal part of changing behaviour — not a cosmetic afterthought.

Before

A function that used to do one thing, and now does five. The name hasn't moved.

<?php

final class UserService
{
    public function validateUser(array $input): array
    {
        if (empty($input['email']) || !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
            return ['ok' => false, 'reason' => 'invalid_email'];
        }

        if (strlen($input['password']) < 8) {
            return ['ok' => false, 'reason' => 'weak_password'];
        }

        $user = [
            'email' => $input['email'],
            'password_hash' => password_hash($input['password'], PASSWORD_DEFAULT),
            'created_at' => date('c'),
        ];

        $this->userRepository->save($user);
        $this->mailer->send($user['email'], 'Welcome!');
        $this->eventBus->publish('user.registered', $user);
        $this->auditLog->record('user_registered', $user['email']);

        return ['ok' => true, 'user' => $user];
    }
}

A caller sees $userService->validateUser($input) in a controller and assumes it is safe to call twice, or safe to call inside a validation-only code path.

It is not. It creates users, sends emails, and writes to an audit log. The name doesn't warn them.

After

The behaviour is named for what it now actually does. If it still needs a validation-only step, that lives as its own function.

<?php

final class UserRegistration
{
    public function registerUser(array $input): array
    {
        $error = $this->findSignupInputError($input);

        if ($error !== null) {
            return ['ok' => false, 'reason' => $error];
        }

        $user = [
            'email' => $input['email'],
            'password_hash' => password_hash($input['password'], PASSWORD_DEFAULT),
            'created_at' => date('c'),
        ];

        $this->userRepository->save($user);
        $this->mailer->send($user['email'], 'Welcome!');
        $this->eventBus->publish('user.registered', $user);
        $this->auditLog->record('user_registered', $user['email']);

        return ['ok' => true, 'user' => $user];
    }

    private function findSignupInputError(array $input): ?string
    {
        if (empty($input['email']) || !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
            return 'invalid_email';
        }

        if (strlen($input['password']) < 8) {
            return 'weak_password';
        }

        return null;
    }
}

Now registerUser() advertises that it registers (with all the side effects that word implies), and findSignupInputError() advertises that it only checks inputs and can be called freely. Readers don't need to open either function to know which one is safe to call twice.

Why this version is better

  • the verb in the name matches the verb the function actually performs
  • side effects no longer hide behind a "safe-sounding" name
  • callers can pick the right function without reading the body
  • the split surfaces the actual two responsibilities the old function had merged together

How names drift without anyone noticing

Every drift starts with a reasonable-looking commit:

  • validateUser grew a ->save() because "we were validating anyway, may as well save"
  • getOrder grew a ->charge() because "we already load the order, save a round trip"
  • sendEmail grew a ->queue() because "actually let's queue it, emails are slow"
  • calculateTotal grew a ->persistCalculation() because "we need this value later"
  • isEligible grew a ->logDecision() because "audit asked for logs"

None of these look wrong in isolation. Added together, they turn every verb in the codebase into a half-truth.

Commit messages are the warning signal

The giveaway sentence in a PR description or a commit is the word "also":

validateUser() now also saves the user.

getOrder() also charges the card if it's unpaid.

sendEmail() also records to the audit log.

Every "also" is a naming debt. You have three honest options:

  1. rename the function to match the new behaviour
  2. split the new behaviour into its own function and call both explicitly
  3. decide the new behaviour doesn't belong here and move it

Silently merging behaviour into a name that no longer fits is not one of the options.

A quick audit for stale names

You can find the worst offenders in a codebase in an hour:

  1. pick the ten most-used public methods (grep for the method name)
  2. for each one, read the body
  3. ask: "if I only knew the name, would I know every effect this has?"
  4. if the answer is no, either rename it or split it

This works especially well for:

  • validate*, check*, is*, has* — boolean-sounding names that now mutate state
  • get*, load*, find*, fetch* — query-sounding names that now write
  • calculate*, compute*, build* — pure-sounding names that now persist
  • process*, handle*, do* — already vague (lesson 2.9), now doubly so

Renaming is a refactor, not a chore

Teams often resist renaming because "the rename touches 40 files". That is exactly the reason to do it — 40 files are currently reading a wrong name.

Modern IDE refactors make symbol renames safe and reversible. Commit the rename on its own, with no behaviour changes. Reviewers can approve it in seconds because the diff is obvious. The follow-up PR then contains the actual behaviour change against an honest name.

This is one of the cheapest habits to adopt and one of the highest-leverage. A codebase where every name still tells the truth is a codebase where readers can trust what they read, and that trust is the foundation everything else in this chapter is trying to build.

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