PHP 中的命名藝術 實用指南
PHP 中的命名藝術 實用指南
命名是計算機科學中最難的兩個問題之一(另外兩個是緩存失效和差一錯誤),時常糾結于 $data 還是 $orderItems 這樣的問題。PHP 也不例外。如果你可以掌握幾個原則、模式和具體例子,命名就能從猜測變成一門手藝。
為什么命名很重要
名字是團隊成員(包括將來的自己)理解系統的第一手資料。一個好名字不僅是標簽,更是意圖的說明:
- 降低認知負擔:讀代碼不需要猜測含義或補全聯想。
- 推動更好的設計:能把職責描述得清楚,通常也意味著設計足夠干凈。
- 降低重構阻力:結構一目了然,動手改動也更從容。
- 縮短新成員上手時間:新人可以直接“閱讀”代碼,而不是拆謎題。
在 PHP 里這一點尤為重要。語言給了我們動態數組、靈活對象和自動加載的自由,如果缺少穩健的命名來約束,這些自由就很容易演變成混亂。
基礎原則
- 清晰勝于簡短:多敲幾個字符,能省下后面無數次的解釋。
- 名字要包含意圖:讓人看名字就知道它是什么、存在的理由是什么。
- 局部保持一致:在同一項目里選一套約定,并且貫徹到底——即便外面的世界更推崇另一套。
- 盡量使用領域語言:代碼里的命名最好能映射業務術語,也就是所謂的“通用語言”。
- 避免誤導:不要把類型塞進名字(例如
$userArray),也不要使用團隊里少有人懂的縮寫。
PHP 基礎:大小寫和約定
- 類與接口:使用 PascalCase,例如
OrderRepository、LoggerInterface。 - 方法與變量:采用 camelCase,例如
calculateTotal()、$orderItems。 - 常量:保持 UPPER_SNAKE_CASE,例如
DEFAULT_TIMEOUT_SECONDS。 - 命名空間:遵循 Vendor\Package\Feature 結構,兼容 PSR-4 自動加載。
- 文件:一份文件對應一個類,文件名與類名保持一致(如
OrderRepository.php)。
這些規則源自 PSR-1 與 PSR-12。真正重要的不是背誦條文,而是在項目內部形成統一的執行標準。
變量:名詞、單位與可信的布爾值
變量總是在描述某個對象、狀態或集合。名稱越貼近那個概念,后續閱讀就越輕松。下面是幾個值得堅持的做法:
用精確的名詞
不好:
$items = $cart->get();
更合適:
$cartItems = $cart->items();
集合用復數,元素用單數
$users = $userRepository->findActive();
foreach ($users as $user) { /* ... */ }
布爾值要像英文一樣讀
用 is、has、can、should、allows、supports 開頭。
$isActive = $user->isActive();
$hasStock = $inventory->hasStock($sku);
$canRefund = $order->canRefund();
避免雙重否定和模糊的標志:
不好:
$notFound = !$found;
$flag = true;
更合適:
$isFound = $repository->exists($id);
$isDraft = $post->isDraft();
在名字里標注單位或幣種
單位寫清楚可以直接消滅一批因換算引發的缺陷。
$timeoutSeconds = 30;
$distanceMeters = 1250;
$amountCents = 1999; // 19.99 美元
別把類型偷偷塞進名字里
不好:
$userArray = getUser(); // 后來其實是個 DTO...
更好:
$user = $userService->currentUser();
避免“雜物箱”式命名
$data、$info、$tmp 這類名字很難傳達任何含義。若命名困難,通常意味著抽象可以再分拆——不妨考慮值對象或 DTO。
函數與方法:動詞、返回值與副作用
函數要么執行操作,要么回答問題。命名時先想清楚它是哪一類,再決定用什么動詞。
查詢 vs 命令(CQS 思想)
查詢:只讀、不產生副作用,適合使用 find、calculate、list 這類描述性的動詞;操作足夠輕量時,可以保留 get。
命令:會改變系統狀態,通常不返回數據或只返回一個成功標記,宜使用 create、update、delete、send、publish 等動作動詞。
// 查詢
$price = $pricing->calculatePrice($cart);
// 命令
$notifier->sendInvoiceEmail($invoiceId);
把 get 留給廉價、同步的訪問
get 通常隱含“即時”“安全”的意味。如果方法需要訪問外部服務或執行復雜邏輯,請改用 fetch、load、retrieve 等更準確的動詞。
$settings = $config->get('checkout'); // 廉價操作
$user = $userRepository->fetchByEmail($email); // I/O(數據庫)
用 ensure 表示冪等的創建邏輯
$apiKey = $keys->ensureExistsFor($userId);
布爾返回值要讀起來像問題
if ($featureGate->isEnabled('new-checkout')) { /* ... */ }
避免“萬能動詞”
不推薦:
processOrder($order); // 怎么處理?
更好:
reserveInventoryFor($order);
chargePaymentFor($order);
generateInvoiceFor($order);
類、接口與 Trait:描述職責,而非實現細節
接口
在公共庫或共享模塊里,習慣在接口名后追加 Interface 后綴:
interface PaymentGatewayInterface
{
public function capture(Money $amount, string $paymentMethodId): CaptureResult;
}
如果是項目內部使用,當前上下文已經清楚表達用途,也可以不加后綴,但要始終如一。
Trait
為 Trait 添加 Trait 后綴,可以避免與實體類混淆:
trait TimestampsTrait
{
// 添加 createdAt / updatedAt 行為
}
抽象類與基類
Abstract 前綴或 Base 后綴都有人采用,但更推薦直接用角色來命名:
abstract class ScheduledTask // 比 AbstractTask 更能說明職責
{
abstract public function run(): void;
}
常見且有意義的后綴
Repository、Factory、Service、Controller、Subscriber、Listener、Specification、Policy、Presenter、Transformer 等后綴都承載著通用的語義。
事件類型通常以 Event 結尾,異常則以 Exception 收尾:
final class OrderPlacedEvent { /* ... */ }
final class PaymentFailedException extends RuntimeException { /* ... */ }
命名空間和目錄:讓 PSR-4 替你打理結構
命名空間層級應與目錄結構一一對應,這樣邏輯分層更清楚,也能減少自動加載配置。
App\
Checkout\
Domain\
Order\
Cart\
Payment\
Application\
PlaceOrder\
RefundOrder\
Infrastructure\
Persistence\
Http\
這種布局讓類名自帶上下文:
App\Checkout\Domain\Order\OrderRepository
App\Checkout\Application\PlaceOrder\PlaceOrderHandler
App\Checkout\Infrastructure\Http\PaymentWebhookController
一看到類型,就能判斷它歸屬哪一層、承擔什么職責。
常量與枚舉:寫出語義,拒絕魔法值
常量
class Cache
{
public const DEFAULT_TTL_SECONDS = 300;
}
枚舉承載狀態(PHP 8.1+)
枚舉把狀態寫成強類型,也讓命名保持唯一解釋。
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);
}
}
把相關邏輯放在枚舉內部,命名自然貼近領域語境。
數組、DTO 和值對象
數組調用方便卻缺乏語義。盡量把匿名結構換成可查的名字。
用具名類型替代“形狀數組”
不推薦:
function createUser(array $data) {
// 期望 ['email' => ..., 'first_name' => ..., 'currency' => ...]
}
更好:
final class CreateUserInput
{
public function __construct(
public string $email,
public string $firstName,
public string $currencyCode
) {}
}
function createUser(CreateUserInput $input) { /* ... */ }
值對象用于領域概念和單位
final class Money
{
public function __construct(
public int $amountCents,
public string $currency
) {}
}
final class EmailAddress
{
public function __construct(public string $value) {
// 在這里驗證格式
}
}
值對象既提供了語義明確的名字(如 Money、EmailAddress),也能在構造時守住不變量。
事件和異常:用后綴傳達語境
事件
領域事件通常用過去式(OrderPlacedEvent、PaymentCapturedEvent)。
應用或集成事件 傾向現在式或命令式(SendNewsletter、UserExportRequested)。
final class OrderPlacedEvent
{
public function __construct(public OrderId $orderId) {}
}
異常
統一以 Exception 收尾,并以違反的業務規則命名,而不是描述表象。
final class InsufficientInventoryException extends DomainException {}
final class PaymentAuthorizationFailedException extends RuntimeException {}
同時提供可操作的消息與上下文;排查時,PaymentAuthorizationFailedException 遠比“payment failed”好用。
數據庫和遷移:讓 SQL 與 PHP 對齊
數據庫世界偏好 snake_case,PHP 則習慣 camelCase。選準映射方案,并且始終如一。
表和列
- 表名:可以用單數或復數,但要統一(
orders或order)。 - 列名:推薦 snake_case(
created_at、user_id)。 - 布爾值:沿用
is_active、has_stock這樣的前綴最易讀。 - 外鍵:保持
user_id、order_id這類格式。
中間表
用表名的字母順序:order_product(不是 product_order),除非你的框架有既定模式。
別把枚舉編碼成魔法整數
傾向于字符串枚舉(status = 'paid')或 FK 到查詢表。在 PHP 端映射到 OrderStatus 枚舉。
遷移名字
讓意圖明顯:
2025_01_15_101500_add_is_active_to_users_table.php
2025_01_20_090000_rename_total_to_subtotal_in_orders_table.php
API 和 CLI 命令:命名你暴露的界面
REST 風格的 HTTP 端點
- 資源用名詞:
/orders、/orders/{id}、/orders/{id}/items - 自定義動作才用動詞:
/orders/{id}/cancel - 查詢參數是篩選條件:
/orders?status=paid&limit=50
JSON 字段
字段命名保持統一(snake_case 或 camelCase)。若內部用 camelCase、外部要 snake_case,請在集中位置做轉換。
CLI 命令
語義要直接、面向任務:
php bin/console orders:rebuild-index
php bin/console users:import --from=legacy.csv
除非領域內已有約定,否則少用 process、handle 這類泛詞。
測試:寫成可讀的文檔
測試面向的讀者僅次于生產代碼。命名清晰的測試在失敗時會直接告訴你發生了什么。
類與文件名
測試類要鏡像被測對象(SUT):OrderRepositoryTest、PlaceOrderHandlerTest。一對一是簡單有效的默認規則。
方法名
任選以下風格之一,并保持一致:
BDD 風格
public function it_calculates_total_for_multiple_items(): void
Given/When/Then 風格(內聯或注解)
public function calculates_total_when_cart_has_discount_voucher(): void
測試輔助與替身
命名突出其職責:
OrderFactory // 測試場景構造器
FakePaymentGateway, StubClock, SpyMailer, InMemoryOrderRepository
數據提供者
/** @dataProvider invalidEmailProvider */
public function it_rejects_invalid_emails(string $email): void { /* ... */ }
注釋與 PHPDoc:名字不夠時
名字承擔 80% 的溝通。把注釋留給另外的 20%:
- 說明為什么,而非重復“做什么”
- 公共 API、復雜不變量用 PHPDoc 記錄前提與約束
- 避免重復簽名已有的類型信息
推薦寫法:
/**
* 按創建順序應用促銷折扣。
* 這保留了合作伙伴依賴的歷史行為。
*/
public function applyDiscounts(Cart $cart): void { /* ... */ }
不推薦寫法:
/** @param int $userId 用戶的 ID */
public function findById(int $userId): User { /* ... */ } // 信息冗余
國際化與語言選擇
- 代碼默認用英文,即便產品對外是本地語言。
- 保留核心領域術語,哪怕它們并非英文(如印尼工資系統里的
BPJSNumber)。 - 面向用戶的文案放進翻譯庫,別寫進標識符。
Composer 包與項目命名
發布到 Packagist 的包名就是公共 API 的一部分:
- Composer 包名用小寫連字符:
acme/payment-gateway - 命名空間通常鏡像它,采用 PascalCase:
Acme\PaymentGateway - 描述務實準確;像
utils、helpers這種籠統名字會迅速過期。
安全地重構名字:技術債會發生
重命名是健康行為。幾條常用守則:
- 使用 IDE 的重構功能,確保引用全部同步。
- 公共接口分階段棄用:保留舊名字,加上
@deprecated并轉發到新實現。 - 在 Changelog 中記錄變更,告知使用方。
- 借助 Rector、PHP CS Fixer 等工具做機械式改名和風格統一。
- 凡是能提升團隊理解的重命名,大多值得投入。
完整端到端示例(從請求到數據庫)
下面用一個緊湊的結賬流程,串起各層命名的協同方式。
領域
namespace App\Checkout\Domain;
final class OrderId
{
public function __construct(public string $value) {}
}
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);
}
}
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;
}
注意動詞:nextId、add、fetchById、listByStatus。沒有通用的 save/get 湯。
應用服務
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);
}
// ...創建并持久化訂單聚合(為簡潔省略)
return new PlaceOrderResult($orderId);
}
private function selectPaymentMethodFor(string $email): string
{
// 領域特定邏輯
return 'card_default';
}
}
名字傳達行為:PlaceOrderCommand(輸入)、PlaceOrderResult(輸出)、handle(應用邊界)、calculateTotal、capture、isSuccessful。
HTTP 層
namespace App\Checkout\Infrastructure\Http;
final class PlaceOrderRequest // 把 JSON 映射到命令
{
/** @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);
}
}
看看名字如何在各層對齊:"place order" 從 HTTP 流向應用再到領域,沒有翻譯的混亂。
數據庫
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
列名鏡像領域術語;沒有 misc_1,沒有 flag。你未來在凌晨 2 點查看 blame 時的自己會感謝你。
命名的棘手角落:幾個可借鑒的模式
區分相似操作
-
remove vs delete:
remove→ 從集合中移除元素delete→ 從持久化層刪除記錄
-
create / register / enroll:選用領域里最自然的詞。
-
update / patch / replace:若遵循 REST 語義,請明確區分。
澄清時間與時區
$expiresAtUtc, $createdAt // 若默認使用 UTC
如需本地時區,請在命名中體現,或使用 ZonedDateTime 這類封裝。
可選 vs 必填
避免使用 maybe、opt 這類前綴;直接通過類型表達(?User、?string)。
若需要三態布爾,用枚舉或狀態值(比如 consentStatus)替代 ?bool。
臨時變量
短暫的循環變量($i、$line)可以接受,其余場景盡量給出有意義的命名。
“Manager / Helper” 氣味
當你想寫 SomethingManager 或 UtilHelper 時,考慮是否可以拆成更具體的小角色,如 TokenGenerator、Slugifier、ChecksumValidator。
代碼審查清單
審查或重命名時,可以自問:
- 這個名字是否使用了業務團隊熟悉的術語?
- 作用域清晰嗎(類 vs 方法 vs 局部變量)?
- 布爾值讀起來像問題嗎?
- 集合名是復數且與元素名匹配嗎?
- 函數名能看出是命令還是查詢嗎?
- 單位、貨幣、時區是否明示?
- 是否存在冗余的類型暗示或誤導性的前綴?
- 不看實現能否猜出職責?
- 是否與周圍命名約定保持一致?
- 如果這是公共 API,這個名字能否經得住時間考驗?
借力工具(不是約束,而是護欄)
- PHP CS Fixer / PHPCS:統一大小寫、文件與類的布局。
- PHPStan / Psalm:通過靜態分析捕捉命名不一致、支持數組形狀。
- Rector:批量完成機械式重命名和棄用流程。
- IDE 檢查:如“未使用”“遮蔽”變量,往往提示命名或設計問題。
- 架構測試:例如用 PHPUnit 斷言“Domain 層不得依賴 Infrastructure”,把命名納入架構邊界。
命名速查表
- 布爾:isX、hasX、canX、shouldX
- 查詢:find、fetch、calculate、list、count
- 命令:create、update、delete、send、publish、reserve
- 集合:復數(
$orders),元素單數($order) - 枚舉:使用單數概念(
OrderStatus::Paid) - 事件:領域事件用過去式(
OrderPlacedEvent) - 異常:以 Exception 結尾,按違反的規則命名
- 單位:加單位后綴(
$timeoutSeconds、$sizeBytes、$amountCents) - 工廠:
FooFactory::createFrom(...)或::from(...) - Repository:add、fetchById、remove、listBy...
前后對比庫(快速獲勝)
命名模糊的變量
// 之前
$info = $service->get($id);
// 之后
$customerProfile = $profileService->fetchById($customerId);
泛化的函數
// 之前
function process($a, $b, $c) { /* ... */ }
// 之后
function generateInvoiceFor(Order $order, TaxRules $rules, Currency $currency): Invoice { /* ... */ }
把副作用藏在 “get” 里
// 之前
$report = $analytics->getMonthlyReport($month); // 會觸發批處理
// 之后
$jobId = $analytics->scheduleMonthlyReport($month);
隱含單位
// 之前
sleep($timeout);
// 之后
sleep($timeoutSeconds);
save 的多重含義
// 之前
$orderRepository->save($order); // 插入?更新?還是 upsert?
// 之后
$orderRepository->add($order); // 寫入新訂單
$orderRepository->update($order); // 更新既有訂單
與遺留代碼和平相處
遇到遺留系統時,可考慮:
- 在邊界層引入適配器名稱,內部逐步使用新命名。
- 把第三方返回的原始數組包裝成具名對象,靠近接口處完成轉換。
- 循序漸進地重命名,從常見方法著手,避免“大手術”。
- 關鍵模塊重命名前寫批準測試(Golden Master)。
框架語境下的命名(Laravel、Symfony 等)
- Laravel Eloquent:模型類默認單數(
Order),表名復數(orders),順勢而為更省力。 - Symfony:偏好顯式服務和構造注入,按職責命名服務(
Slugifier、OrderNumberGenerator)讓容器一目了然。 - 事件/監聽器:各框架略有差異,遵循框架約定可減少手工配置。
約定是一種“免費”的集成方式,善用它來降低儀式感。
常見命名陷阱
- “Manager / Helper / Util” 大雜燴:通常意味著缺乏清晰抽象。
- 匈牙利命名法:2025 年不再需要
$strName;類型系統與 IDE 會幫你。 - 模糊縮寫:若必須使用,請文檔化并保持一致(SKU、VAT、OTP)。
- 過度前綴:在
OrderService內無需orderServiceProcessOrder()。 - UI 驅動的命名:
BlueButtonHandler在換皮后立刻過時。 - 時間命名:
newOrder、tempUser、finalData很快就會說謊。
設立團隊約定
命名確實包含品味,但團隊共識可以降低摩擦:
- 撰寫簡明的命名 ADR(Architecture Decision Record)。
- 在倉庫內提供示例(如
/docs/naming.md)。 - 在 PR 中鼓勵提出具體替代方案(“建議用
PaymentCaptureResult,因為它描述的是捕獲結果而非 HTTP 響應”)。 - 定期復盤(例如季度),隨著經驗更新約定。
結語
好名字無法拯救糟糕的設計,卻能讓良好的設計發光,并降低未來重構的成本。PHP 給了你足夠的靈活性,用命名把這種靈活性轉化為清晰度。
從小處著手:重命名一個模糊變量,拆分一個泛泛的函數,引入一個值對象替換數組。持續迭代,幾周之內你就能感受到代碼庫變得更輕、更清楚、更友好。

浙公網安備 33010602011771號