← Back to list

Naming Things in PHP: A Practical Guide

Practical patterns, PSR-friendly conventions, and real examples for clearer PHP code

Ann R. · 2025-10-30 19:17 · 172 claps · 11.5 min read paywalled
#php #php-development #naming #psr #php-enum
Open on Medium ↗
Wiki topics: GEN · Genomics & Sequencing

Naming Things in PHP: A Practical Guide

Photo by Tim Mossholder on Unsplash

Photo by Tim Mossholder on Unsplash

If you’ve ever paused mid-commit thinking, “Should this be $data or $orderItems?”—welcome to the club. Naming is one of the two hard problems in computer science (the other two being cache invalidation and off-by-one errors), and PHP is no exception. The good news: with a few principles, patterns, and concrete examples, you can make naming feel less like guesswork and more like a craft.

Names are the primary interface your teammates (and your future self) use to understand the system. Clear names:

  • Reduce cognitive load (no decoding every line).
  • Encourage better designs (you tend to build simpler things when you can explain them).
  • Make refactoring cheaper (the structure is obvious, so change is less scary).
  • Improve onboarding (new devs read code like a book instead of solving a puzzle).

In PHP specifically, the language gives you a lot of freedom — dynamic arrays, loose objects, flexible autoloading — and freedom can quickly turn into chaos if names aren’t doing heavy lifting.

Ground rules (keep these in your back pocket)

  1. Prefer clarity over brevity. A few extra characters now can save a thousand Slack messages later.
  2. Names should reveal intent. What is this thing and why does it exist?
  3. Be consistent locally. Pick a convention and stick to it within a project (even if the internet prefers something else).
  4. Use the domain’s language. Mirror business terms (a.k.a. the “ubiquitous language”) in code.
  5. Avoid misleading cues. Don’t put types in names ($userArray) or use abbreviations no one else understands.

The PHP basics: casing and conventions that keep you out of trouble

  • Classes & Interfaces: PascalCase (OrderRepository, LoggerInterface)
  • Methods & Variables: camelCase (calculateTotal(), $orderItems)
  • Constants: UPPER_SNAKE_CASE (DEFAULT_TIMEOUT_SECONDS)
  • Namespaces: Vendor\Package\Feature (align with PSR-4 autoloading)
  • Files: One class per file, file named after the class (OrderRepository.php)

These match the common PSR guidelines (PSR-1/PSR-12), but more important than the standard is consistency inside your codebase.

Variables: nouns, units, and honest booleans

Variables represent things — so name them like things. A few patterns that pay off:

Prefer precise nouns

Bad:

$items = $cart->get();

Better:

$cartItems = $cart->items();

Collections are plural, items are singular

$users = $userRepository->findActive();
foreach ($users as $user) { /* ... */ }

Booleans read like English

Use is, has, can, should, allows, supports.

$isActive = $user->isActive();
$hasStock = $inventory->hasStock($sku);
$canRefund = $order->canRefund();

Avoid double negatives and vague flags:

Bad:

$notFound = !$found;
$flag = true;

Better:

$isFound = $repository->exists($id);
$isDraft = $post->isDraft();

Include units and currencies in names

You will absolutely save yourself bugs here.

$timeoutSeconds = 30;
$distanceMeters = 1250;
$amountCents = 1999; // 19.99 in USD

Don’t smuggle types into names

Bad:

$userArray = getUser(); // it's actually a DTO later…

Better:

$user = $userService->currentUser();

Avoid junk drawer names ($data, $info, $tmp)

If you can’t name it well, your abstraction might be wrong. Consider introducing a value object or a DTO.

Functions & methods: verbs, result types, and side effects

Functions do things or answer questions. Name them accordingly.

Queries vs Commands (CQS mindset)

  • Query: returns data, no side effects → noun or descriptive verb (find, calculate, list, get when cheap)
  • Command: causes change, no return (or returns success) → action verb (create, update, delete, send, publish)
// Query
$price = $pricing->calculatePrice($cart);
// Command
$notifier->sendInvoiceEmail($invoiceId);

Reserve get for cheap, synchronous access

get implies instant and side-effect-free. If you’re hitting I/O or doing work, choose fetch, load, or retrieve.

$settings = $config->get('checkout');   // cheap
$user = $userRepository->fetchByEmail($email); // I/O (DB)

Use “ensure” for idempotent creation

$apiKey = $keys->ensureExistsFor($userId);

Boolean-returning methods should read like questions

if ($featureGate->isEnabled('new-checkout')) { /* ... */ }

Avoid overly generic verbs

Bad:

processOrder($order); // Process how?

Better:

reserveInventoryFor($order);
chargePaymentFor($order);
generateInvoiceFor($order);

Classes, interfaces, and traits: describe roles, not mechanics

Interfaces

In libraries and shared code, appending Interface is common and helpful:

interface PaymentGatewayInterface
{
    public function capture(Money $amount, string $paymentMethodId): CaptureResult;
}

In internal codebases you can skip the suffix if the context is crystal clear, but be consistent.

Traits

Use the Trait suffix to avoid confusion with classes:

trait TimestampsTrait
{
    // adds createdAt/updatedAt behavior
}

Abstract/Base classes

Both Abstract (prefix) and Base (suffix) appear in the wild. Use sparingly, and prefer naming the role:

abstract class ScheduledTask // clearer than AbstractTask
{
    abstract public function run(): void;
}

Pattern suffixes worth keeping

  • Repository, Factory, Service, Controller, Subscriber, Listener, Specification, Policy, Presenter, Transformer
  • Event classes often end with Event; exceptions end with Exception.
final class OrderPlacedEvent { /* ... */ }
final class PaymentFailedException extends RuntimeException { /* ... */ }

Namespaces & directories: let PSR-4 do the heavy lifting

Match namespace segments to directories and choose names that reflect your domain and architecture.

App\
  Checkout\
    Domain\
      Order\
      Cart\
      Payment\
    Application\
      PlaceOrder\
      RefundOrder\
    Infrastructure\
      Persistence\
      Http\

A structure like this makes names self-documenting:

  • App\Checkout\Domain\Order\OrderRepository
  • App\Checkout\Application\PlaceOrder\PlaceOrderHandler
  • App\Checkout\Infrastructure\Http\PaymentWebhookController

When you see a type, you already know its “neighborhood.”

Constants & enums: encode meaning, not magic values

Constants: shout in SNAKE_CASE

class Cache
{
    public const DEFAULT_TTL_SECONDS = 300;
}

Enums for states (PHP 8.1+)

Enums make names first-class and eliminate stringly-typed bugs.

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function isFinal(): bool
    {
        return in_array($this, [self::Shipped, self::Cancelled], true);
    }
}

Use methods on enums to keep logic near the concept; naming then flows from the domain.

Arrays, DTOs, and value objects: stop passing “array soup”

Arrays are convenient but anonymous. Prefer names you can rely on.

Replace “shape arrays” with named types

Bad:

function createUser(array $data) {
    // expects ['email' => ..., 'first_name' => ..., 'currency' => ...]
}

Better:

final class CreateUserInput
{
    public function __construct(
        public string $email,
        public string $firstName,
        public string $currencyCode
    ) {}
}
function createUser(CreateUserInput $input) { /* ... */ }

Value objects for domain concepts and units

final class Money
{
    public function __construct(
        public int $amountCents,
        public string $currency
    ) {}
}
final class EmailAddress
{
    public function __construct(public string $value) {
        // validate format here
    }
}

Value objects give you meaningful names (Money, EmailAddress) and safeguard invariants.

Events and exceptions: semantics in the suffix

Events

  • Domain events: past tense (OrderPlacedEvent, PaymentCapturedEvent).
  • Application/integration events: present or imperative (SendNewsletter, UserExportRequested).
final class OrderPlacedEvent
{
    public function __construct(public OrderId $orderId) {}
}

Exceptions

End with Exception. Name them after the rule that was violated, not the symptom.

final class InsufficientInventoryException extends DomainException {}
final class PaymentAuthorizationFailedException extends RuntimeException {}

Include actionable messages and context. It’s easier to grep for PaymentAuthorizationFailedException than "payment failed".

Database & migrations: PHP names meet SQL names

Databases often favor snake_case; PHP favors camelCase. Pick a mapping and stick to it.

Tables and columns

  • Tables: plural or singular — just be consistent (orders or order).
  • Columns: snake_case is common (created_at, user_id)
  • Booleans: is_active, has_stock.
  • Foreign keys: user_id, order_id.

Pivot tables

Use alphabetical order of table names: order_product (not product_order), unless your framework has an established pattern you follow.

Don’t encode enums as magic integers

Prefer string enums (status = 'paid') or FK to a lookup table. On the PHP side, map to OrderStatus enum.

Migration names

Make intent obvious:

2025_01_15_101500_add_is_active_to_users_table.php
2025_01_20_090000_rename_total_to_subtotal_in_orders.php

APIs and CLI commands: name the surface you expose

REST-ish HTTP endpoints

  • Nouns for resources: /orders, /orders/{id}, /orders/{id}/items.
  • Use verbs for custom actions only when necessary: /orders/{id}/cancel.
  • Query params are filters: /orders?status=paid&limit=50.

JSON fields

Use a consistent case (snake_case or camelCase). If PHP uses camelCase but you expose snake_case JSON, centralize the transformation.

CLI commands

Keep them task-oriented and explicit:

php bin/console orders:rebuild-index
php bin/console users:import --from=legacy.csv

Avoid ambiguous verbs like process or handle unless the domain already uses them precisely.

Tests: write names that read like documentation

Tests are your second most important audience after production code. Good names make test failures obvious.

Class and file names

  • Class mirrors the SUT (system under test): OrderRepositoryTest, PlaceOrderHandlerTest.
  • One test class per SUT is a solid default.

Method names

Two proven styles — pick one and stick with it:

BDD style

public function it_calculates_total_for_multiple_items(): void

Given/When/Then style (inline or annotations)

public function calculates_total_when_cart_has_discount_voucher(): void

Fixtures and doubles

Name the role, not the mechanics:

  • OrderFactory (test helper for building orders)
  • FakePaymentGateway, StubClock, SpyMailer, InMemoryOrderRepository

Data providers

/** @dataProvider invalidEmailProvider */
public function it_rejects_invalid_emails(string $email): void { /* ... */ }

Comments & PHPDoc: when names aren’t enough

Names should carry 80% of the meaning. Save comments for the other 20%:

  • Document why, not what.
  • Use PHPDoc for public APIs and complex invariants.
  • Don’t duplicate type info already in signatures (especially with scalar and class types).

Good:

/**
 * Applies promotional discounts in the order they were created.
 * This preserves historical behavior relied upon by partners.
 */
public function applyDiscounts(Cart $cart): void { /* ... */ }

Avoid:

/** @param int $userId The ID of the user */
public function findById(int $userId): User { /* ... */ } // redundant

Internationalization & language choices

  • Code names in English by default, even if your product is localized.
  • It’s fine to use domain-specific terms that aren’t English if your business universally uses them (e.g., BPJSNumber in an Indonesian payroll system).
  • For user-facing strings, keep them out of names and in translation files.

Composer package & project naming

When you publish PHP packages, the vendor/package name is part of your API surface.

  • Use lowercase with dashes for Composer: acme/payment-gateway.
  • The namespace typically mirrors it in PascalCase: Acme\PaymentGateway.
  • Keep the description honest; names like utils or helpers age poorly.

Refactoring names safely: debt happens, plan for it

You will rename things. That’s healthy. A few tactics:

  • Use your IDE’s rename refactor to update references safely.
  • Deprecate gradually in libraries: keep the old name with @deprecated and a shim that forwards to the new one.
  • Changelog entries for renamed public types and methods.
  • Rector and PHP CS Fixer can automate mechanical renames and enforce style.

If a rename improves clarity for the whole team, it’s almost always worth doing.

A mini end-to-end example (from request to database)

Let’s wire together a small checkout flow and focus purely on names.

Domain

namespace App\Checkout\Domain;
final class OrderId
{
    public function __construct(public string $value) {}
}
enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
    public function isFinal(): bool
    {
        return in_array($this, [self::Paid, self::Cancelled], true);
    }
}
final class Money
{
    public function __construct(
        public int $amountCents,
        public string $currency
    ) {}
}

Repository

namespace App\Checkout\Domain;
interface OrderRepository
{
    public function nextId(): OrderId;
    public function add(Order $order): void;
    public function fetchById(OrderId $id): ?Order;
    /** @return list<Order> */
    public function listByStatus(OrderStatus $status, int $limit = 50): array;
}

Note the verbs: nextId, add, fetchById, listByStatus. No generic save/get soup.

Application service

namespace App\Checkout\Application\PlaceOrder;
use App\Checkout\Domain\{OrderRepository, Money, OrderId};
final class PlaceOrderCommand
{
    /** @param list<string> $productSkus */
    public function __construct(
        public string $customerEmail,
        public array $productSkus,
        public string $currency
    ) {}
}
final class PlaceOrderResult
{
    public function __construct(public OrderId $orderId) {}
}
final class PlaceOrderHandler
{
    public function __construct(
        private OrderRepository $orders,
        private PricingService $pricing,
        private PaymentGatewayInterface $payments,
    ) {}
    public function handle(PlaceOrderCommand $cmd): PlaceOrderResult
    {
        $orderId = $this->orders->nextId();
        $total = $this->pricing->calculateTotal(
            $cmd->productSkus,
            $cmd->currency
        );
        $capture = $this->payments->capture(
            new Money($total->amountCents, $total->currency),
            paymentMethodId: $this->selectPaymentMethodFor($cmd->customerEmail)
        );
        if (!$capture->isSuccessful()) {
            throw new PaymentAuthorizationFailedException($capture->reason);
        }
        // ...create & persist order aggregate (omitted for brevity)
        return new PlaceOrderResult($orderId);
    }
    private function selectPaymentMethodFor(string $email): string
    {
        // domain-specific logic
        return 'card_default';
    }
}

Names telegraph behavior: PlaceOrderCommand (input), PlaceOrderResult (output), handle (application boundary), calculateTotal, capture, isSuccessful.

HTTP layer

namespace App\Checkout\Infrastructure\Http;

final class PlaceOrderRequest // maps JSON to command
{
    /** @param list<string> $productSkus */
    public function __construct(
        public string $customerEmail,
        public array $productSkus,
        public string $currency
    ) {}
}

final class PlaceOrderController
{
    public function __construct(private PlaceOrderHandler $handler) {}
    public function __invoke(Request $request): Response
    {
        $payload = new PlaceOrderRequest(
            customerEmail: $request->get('customer_email'),
            productSkus: $request->get('product_skus'),
            currency: $request->get('currency', 'USD'),
        );
        $result = $this->handler->handle(new PlaceOrderCommand(
            $payload->customerEmail,
            $payload->productSkus,
            $payload->currency
        ));
        return new JsonResponse(['order_id' => $result->orderId->value], 201);
    }
}

Notice how names align across layers: “place order” flows from HTTP to Application to Domain without translation whiplash.

Database (migration sketch)

orders
  id              CHAR(26) PRIMARY KEY
  customer_email  VARCHAR(255) NOT NULL
  status          ENUM('pending','paid','cancelled') NOT NULL
  total_cents     INT NOT NULL
  currency        CHAR(3) NOT NULL
  created_at      DATETIME NOT NULL

Columns mirror domain terms; no misc_1, no flag. Your future migration-running self says thanks.

Naming tricky corners: a few patterns to steal

Distinguish similar operations

  • remove vs delete:
  • remove → from a collection;
  • delete → from persistence.
  • create vs register vs enroll → pick the word your domain uses.
  • update vs patch vs replace → be explicit if you follow REST semantics.

Clarify time and zones

  • expiresAtUtc, createdAt (if always UTC)
  • If not UTC, embed zone or use value objects (ZonedDateTime—you can wrap DateTimeImmutable).

Optional vs required

  • Avoid maybe or opt prefixes; reflect optionality with types (?User, ?string).
  • For nullable booleans, clarify with tri-state names: consentStatus as enum rather than ?bool.

Temporary values

Short-lived loop variables are fine ($i, $line). Everything else deserves a real name.

“Manager” and “Helper” smell

If you’re about to write SomethingManager or UtilHelper, pause. The name might be hiding a design that wants smaller, role-specific classes: TokenGenerator, Slugifier, ChecksumValidator.

Code review checklist (print this)

When reviewing or renaming, ask:

  1. Does the name reflect the domain term users or stakeholders use?
  2. Is the scope clear? (class vs method vs local variable)
  3. For booleans, does it read like a question?
  4. For collections, is it plural and consistent with item naming?
  5. For functions, is it a command or a query?
  6. Are units/currency/time zones explicit where needed?
  7. Is the name free from redundant type hints or misleading prefixes?
  8. Would a newcomer guess what this does without opening the implementation?
  9. Does it align with nearby names and conventions?
  10. If this were public API, would you bet on it aging well?

Tooling that reinforces good names (without being a tyrant)

  • PHP CS Fixer / PHPCS: enforce casing and file/class layout.
  • PHPStan / Psalm: catch mismatches that better names would have revealed, support array shapes if you can’t avoid arrays.
  • Rector: automate mechanical renames and deprecations.
  • IDE inspections: “unused” or “shadowed” variables often hint at naming/design problems.
  • Architecture tests (e.g., PHPUnit with custom assertions): “classes in Domain must not depend on Infrastructure,” which keeps naming aligned with boundaries.

A quick naming cheat-sheet

  • Booleans: isX, hasX, canX, shouldX.
  • Queries: find, fetch, calculate, list, count.
  • Commands: create, update, delete, send, publish, reserve.
  • Collections: plural ($orders), item singular ($order).
  • Enums: singular concept (OrderStatus::Paid).
  • Events: past tense for domain (OrderPlacedEvent).
  • Exceptions: end with Exception, name the violated rule.
  • Units: suffix with unit ($timeoutSeconds, $sizeBytes, $amountCents).
  • Factories: FooFactory::createFrom(...) or ::from(...) static constructors.
  • Repositories: add, fetchById, remove, listBy....

Before/after gallery (fast wins you can copy)

1) Vague variable

// Before
$info = $service->get($id);
// After
$customerProfile = $profileService->fetchById($customerId);

2) Generic function

// Before
function process($a, $b, $c) { /* ... */ }
// After
function generateInvoiceFor(Order $order, TaxRules $rules, Currency $currency): Invoice { /* ... */ }

3) Side-effects hidden in a “get”

// Before
$report = $analytics->getMonthlyReport($month); // sends batch job!
// After
$jobId = $analytics->scheduleMonthlyReport($month);

4) Implicit units

// Before
sleep($timeout);
// After
sleep($timeoutSeconds);

5) Overloaded “save”

// Before
$orderRepository->save($order); // insert? update? upsert?
// After
$orderRepository->add($order);     // new
$orderRepository->update($order);  // existing

Handling legacy code without losing your mind

You’ll meet code that doesn’t follow any of this. A few tactics:

  • Introduce Adapter names at the edges: keep legacy names on one side, new names on your side.
  • Wrap raw arrays returned by legacy libraries into small, named objects near the boundary.
  • Rename incrementally: start with method names you touch often; avoid “rename the world” PRs.
  • Write approval tests (golden master) before big renames in critical modules.

Framework notes (Laravel, Symfony, etc.) — use idioms wisely

  • Laravel Eloquent leans toward singular model classes (Order) with plural tables (orders). Embrace its conventions to avoid friction.
  • Symfony loves explicit services and constructor injection; naming services after roles (Slugifier, OrderNumberGenerator) keeps your container readable.
  • Event/Listener naming differs slightly per framework; align with the framework’s defaults so auto-wiring “just works.”

Conventions buy you free integration and less configuration. Choose the idiom that reduces ceremony.

Common pitfalls to watch for

  • “Manager/Helper/Util” catch-alls. Usually a code smell for missing concepts.
  • Hungarian notation. No need for $strName in 2025; types and IDEs have you covered.
  • Ambiguous acronyms. If you must use them, document and be consistent (SKU, VAT, OTP).
  • Over-prefixing. Don’t repeat context: inside OrderService, you don’t need orderServiceProcessOrder().
  • Names that encode UI rather than domain. BlueButtonHandler won’t age well after the redesign.
  • Temporal names. newOrder, tempUser, finalData—guaranteed to lie in a month.

“But isn’t this subjective?” — yes, so set team conventions

Naming involves taste. That’s why the real superpower is agreement:

  • Write a short naming ADR (Architecture Decision Record).
  • Put examples right in the repo (/docs/naming.md).
  • Encourage pull request comments that propose specific alternatives (“PaymentCaptureResult instead of PaymentResponse because it’s not an HTTP thing”).
  • Revisit every quarter; change when you learn something better.

Conclusion: your future self is your most important reader

Good names won’t make a bad design good, but they’ll make a good design shine — and they’ll make changing your mind possible. PHP gives you flexible building blocks. Use names to turn that flexibility into clarity.

Start small: rename one vague variable, split one generic method into two well-named ones, introduce one value object to banish an array shape. Do it repeatedly, and in a few weeks your codebase will feel different to work in — lighter, clearer, friendlier.

And remember: if you’re hesitating between a short, cryptic name and a longer, honest one, choose honesty. Your teammates (and your future self browsing blame at 2 a.m.) will thank you.


메타데이터
post_id
483c70cadc8a
slug
naming-things-in-php-a-practical-guide-483c70cadc8a
url
https://medium.com/@annxsa/naming-things-in-php-a-practical-guide-483c70cadc8a
canonical_url
https://medium.com/@annxsa/naming-things-in-php-a-practical-guide-483c70cadc8a
author_url
https://medium.com/@annxsa
status
ok
fetched_at
2026-06-21 07:44:09