Naming Things in PHP: A Practical Guide
Practical patterns, PSR-friendly conventions, and real examples for clearer PHP code
Naming Things in PHP: A Practical Guide
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)
- Prefer clarity over brevity. A few extra characters now can save a thousand Slack messages later.
- Names should reveal intent. What is this thing and why does it exist?
- Be consistent locally. Pick a convention and stick to it within a project (even if the internet prefers something else).
- Use the domain’s language. Mirror business terms (a.k.a. the “ubiquitous language”) in code.
- 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,getwhen 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 withException.
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\OrderRepositoryApp\Checkout\Application\PlaceOrder\PlaceOrderHandlerApp\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 (
ordersororder). - Columns:
snake_caseis 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.,
BPJSNumberin 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
utilsorhelpersage 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
@deprecatedand 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
removevsdelete:remove→ from a collection;delete→ from persistence.createvsregistervsenroll→ pick the word your domain uses.updatevspatchvsreplace→ 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 wrapDateTimeImmutable).
Optional vs required
- Avoid
maybeoroptprefixes; reflect optionality with types (?User,?string). - For nullable booleans, clarify with tri-state names:
consentStatusas 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:
- Does the name reflect the domain term users or stakeholders use?
- Is the scope clear? (class vs method vs local variable)
- For booleans, does it read like a question?
- For collections, is it plural and consistent with item naming?
- For functions, is it a command or a query?
- Are units/currency/time zones explicit where needed?
- Is the name free from redundant type hints or misleading prefixes?
- Would a newcomer guess what this does without opening the implementation?
- Does it align with nearby names and conventions?
- 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
Domainmust not depend onInfrastructure,” 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
$strNamein 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 needorderServiceProcessOrder(). - Names that encode UI rather than domain.
BlueButtonHandlerwon’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 (“
PaymentCaptureResultinstead ofPaymentResponsebecause 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