PHP 中的命名艺术 实用指南

PHP 中的命名艺术 实用指南

命名是计算机科学中最难的两个问题之一(另外两个是缓存失效和差一错误),时常纠结于 $data 还是 $orderItems 这样的问题。PHP 也不例外。如果你可以掌握几个原则、模式和具体例子,命名就能从猜测变成一门手艺。

原文链接 PHP 中的命名艺术 实用指南

为什么命名很重要

名字是团队成员(包括将来的自己)理解系统的第一手资料。一个好名字不仅是标签,更是意图的说明:

  • 降低认知负担:读代码不需要猜测含义或补全联想。
  • 推动更好的设计:能把职责描述得清楚,通常也意味着设计足够干净。
  • 降低重构阻力:结构一目了然,动手改动也更从容。
  • 缩短新成员上手时间:新人可以直接"阅读"代码,而不是拆谜题。

在 PHP 里这一点尤为重要。语言给了我们动态数组、灵活对象和自动加载的自由,如果缺少稳健的命名来约束,这些自由就很容易演变成混乱。

基础原则

  • 清晰胜于简短:多敲几个字符,能省下后面无数次的解释。
  • 名字要包含意图:让人看名字就知道它是什么、存在的理由是什么。
  • 局部保持一致:在同一项目里选一套约定,并且贯彻到底------即便外面的世界更推崇另一套。
  • 尽量使用领域语言:代码里的命名最好能映射业务术语,也就是所谓的"通用语言"。
  • 避免误导 :不要把类型塞进名字(例如 $userArray),也不要使用团队里少有人懂的缩写。

PHP 基础:大小写和约定

  • 类与接口 :使用 PascalCase,例如 OrderRepositoryLoggerInterface
  • 方法与变量 :采用 camelCase,例如 calculateTotal()$orderItems
  • 常量 :保持 UPPER_SNAKE_CASE,例如 DEFAULT_TIMEOUT_SECONDS
  • 命名空间:遵循 Vendor\Package\Feature 结构,兼容 PSR-4 自动加载。
  • 文件 :一份文件对应一个类,文件名与类名保持一致(如 OrderRepository.php)。

这些规则源自 PSR-1 与 PSR-12。真正重要的不是背诵条文,而是在项目内部形成统一的执行标准。

变量:名词、单位与可信的布尔值

变量总是在描述某个对象、状态或集合。名称越贴近那个概念,后续阅读就越轻松。下面是几个值得坚持的做法:

用精确的名词

不好:

php 复制代码
$items = $cart->get();

更合适:

php 复制代码
$cartItems = $cart->items();

集合用复数,元素用单数

php 复制代码
$users = $userRepository->findActive();
foreach ($users as $user) { /* ... */ }

布尔值要像英文一样读

ishascanshouldallowssupports 开头。

php 复制代码
$isActive = $user->isActive();
$hasStock = $inventory->hasStock($sku);
$canRefund = $order->canRefund();

避免双重否定和模糊的标志:

不好:

php 复制代码
$notFound = !$found;
$flag = true;

更合适:

php 复制代码
$isFound = $repository->exists($id);
$isDraft = $post->isDraft();

在名字里标注单位或币种

单位写清楚可以直接消灭一批因换算引发的缺陷。

php 复制代码
$timeoutSeconds = 30;
$distanceMeters = 1250;
$amountCents = 1999; // 19.99 美元

别把类型偷偷塞进名字里

不好:

php 复制代码
$userArray = getUser(); // 后来其实是个 DTO...

更好:

php 复制代码
$user = $userService->currentUser();

避免"杂物箱"式命名

$data$info$tmp 这类名字很难传达任何含义。若命名困难,通常意味着抽象可以再分拆------不妨考虑值对象或 DTO。

函数与方法:动词、返回值与副作用

函数要么执行操作,要么回答问题。命名时先想清楚它是哪一类,再决定用什么动词。

查询 vs 命令(CQS 思想)

查询 :只读、不产生副作用,适合使用 find、calculate、list 这类描述性的动词;操作足够轻量时,可以保留 get

命令:会改变系统状态,通常不返回数据或只返回一个成功标记,宜使用 create、update、delete、send、publish 等动作动词。

php 复制代码
// 查询
$price = $pricing->calculatePrice($cart);

// 命令
$notifier->sendInvoiceEmail($invoiceId);

get 留给廉价、同步的访问

get 通常隐含"即时""安全"的意味。如果方法需要访问外部服务或执行复杂逻辑,请改用 fetchloadretrieve 等更准确的动词。

php 复制代码
$settings = $config->get('checkout');   // 廉价操作
$user = $userRepository->fetchByEmail($email); // I/O(数据库)

ensure 表示幂等的创建逻辑

php 复制代码
$apiKey = $keys->ensureExistsFor($userId);

布尔返回值要读起来像问题

php 复制代码
if ($featureGate->isEnabled('new-checkout')) { /* ... */ }

避免"万能动词"

不推荐:

php 复制代码
processOrder($order); // 怎么处理?

更好:

php 复制代码
reserveInventoryFor($order);
chargePaymentFor($order);
generateInvoiceFor($order);

类、接口与 Trait:描述职责,而非实现细节

接口

在公共库或共享模块里,习惯在接口名后追加 Interface 后缀:

php 复制代码
interface PaymentGatewayInterface
{
    public function capture(Money $amount, string $paymentMethodId): CaptureResult;
}

如果是项目内部使用,当前上下文已经清楚表达用途,也可以不加后缀,但要始终如一。

Trait

为 Trait 添加 Trait 后缀,可以避免与实体类混淆:

php 复制代码
trait TimestampsTrait
{
    // 添加 createdAt / updatedAt 行为
}

抽象类与基类

Abstract 前缀或 Base 后缀都有人采用,但更推荐直接用角色来命名:

php 复制代码
abstract class ScheduledTask // 比 AbstractTask 更能说明职责
{
    abstract public function run(): void;
}

常见且有意义的后缀

Repository、Factory、Service、Controller、Subscriber、Listener、Specification、Policy、Presenter、Transformer 等后缀都承载着通用的语义。

事件类型通常以 Event 结尾,异常则以 Exception 收尾:

php 复制代码
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

一看到类型,就能判断它归属哪一层、承担什么职责。

常量与枚举:写出语义,拒绝魔法值

常量

php 复制代码
class Cache
{
    public const DEFAULT_TTL_SECONDS = 300;
}

枚举承载状态(PHP 8.1+)

枚举把状态写成强类型,也让命名保持唯一解释。

php 复制代码
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 和值对象

数组调用方便却缺乏语义。尽量把匿名结构换成可查的名字。

用具名类型替代"形状数组"

不推荐:

php 复制代码
function createUser(array $data) {
    // 期望 ['email' => ..., 'first_name' => ..., 'currency' => ...]
}

更好:

php 复制代码
final class CreateUserInput
{
    public function __construct(
        public string $email,
        public string $firstName,
        public string $currencyCode
    ) {}
}

function createUser(CreateUserInput $input) { /* ... */ }

值对象用于领域概念和单位

php 复制代码
final class Money
{
    public function __construct(
        public int $amountCents,
        public string $currency
    ) {}
}

final class EmailAddress
{
    public function __construct(public string $value) {
        // 在这里验证格式
    }
}

值对象既提供了语义明确的名字(如 MoneyEmailAddress),也能在构造时守住不变量。

事件和异常:用后缀传达语境

事件

领域事件 通常用过去式(OrderPlacedEventPaymentCapturedEvent)。

应用或集成事件 倾向现在式或命令式(SendNewsletterUserExportRequested)。

php 复制代码
final class OrderPlacedEvent
{
    public function __construct(public OrderId $orderId) {}
}

异常

统一以 Exception 收尾,并以违反的业务规则命名,而不是描述表象。

php 复制代码
final class InsufficientInventoryException extends DomainException {}
final class PaymentAuthorizationFailedException extends RuntimeException {}

同时提供可操作的消息与上下文;排查时,PaymentAuthorizationFailedException 远比"payment failed"好用。

数据库和迁移:让 SQL 与 PHP 对齐

数据库世界偏好 snake_case,PHP 则习惯 camelCase。选准映射方案,并且始终如一。

表和列

  • 表名 :可以用单数或复数,但要统一(ordersorder)。
  • 列名 :推荐 snake_case(created_atuser_id)。
  • 布尔值 :沿用 is_activehas_stock 这样的前缀最易读。
  • 外键 :保持 user_idorder_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

除非领域内已有约定,否则少用 processhandle 这类泛词。

测试:写成可读的文档

测试面向的读者仅次于生产代码。命名清晰的测试在失败时会直接告诉你发生了什么。

类与文件名

测试类要镜像被测对象(SUT):OrderRepositoryTestPlaceOrderHandlerTest。一对一是简单有效的默认规则。

方法名

任选以下风格之一,并保持一致:

BDD 风格

php 复制代码
public function it_calculates_total_for_multiple_items(): void

Given/When/Then 风格(内联或注解)

php 复制代码
public function calculates_total_when_cart_has_discount_voucher(): void

测试辅助与替身

命名突出其职责:

php 复制代码
OrderFactory // 测试场景构造器
FakePaymentGateway, StubClock, SpyMailer, InMemoryOrderRepository

数据提供者

php 复制代码
/** @dataProvider invalidEmailProvider */
public function it_rejects_invalid_emails(string $email): void { /* ... */ }

注释与 PHPDoc:名字不够时

名字承担 80% 的沟通。把注释留给另外的 20%:

  • 说明为什么,而非重复"做什么"
  • 公共 API、复杂不变量用 PHPDoc 记录前提与约束
  • 避免重复签名已有的类型信息

推荐写法:

php 复制代码
/**
 * 按创建顺序应用促销折扣。
 * 这保留了合作伙伴依赖的历史行为。
 */
public function applyDiscounts(Cart $cart): void { /* ... */ }

不推荐写法:

php 复制代码
/** @param int $userId 用户的 ID */
public function findById(int $userId): User { /* ... */ } // 信息冗余

国际化与语言选择

  • 代码默认用英文,即便产品对外是本地语言。
  • 保留核心领域术语 ,哪怕它们并非英文(如印尼工资系统里的 BPJSNumber)。
  • 面向用户的文案放进翻译库,别写进标识符。

Composer 包与项目命名

发布到 Packagist 的包名就是公共 API 的一部分:

  • Composer 包名用小写连字符acme/payment-gateway
  • 命名空间通常镜像它,采用 PascalCaseAcme\PaymentGateway
  • 描述务实准确 ;像 utilshelpers 这种笼统名字会迅速过期。

安全地重构名字:技术债会发生

重命名是健康行为。几条常用守则:

  • 使用 IDE 的重构功能,确保引用全部同步。
  • 公共接口分阶段弃用 :保留旧名字,加上 @deprecated 并转发到新实现。
  • 在 Changelog 中记录变更,告知使用方。
  • 借助 Rector、PHP CS Fixer 等工具做机械式改名和风格统一。
  • 凡是能提升团队理解的重命名,大多值得投入。

完整端到端示例(从请求到数据库)

下面用一个紧凑的结账流程,串起各层命名的协同方式。

领域

php 复制代码
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

php 复制代码
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;
}

注意动词:nextIdaddfetchByIdlistByStatus。没有通用的 save/get 汤。

应用服务

php 复制代码
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(应用边界)、calculateTotalcaptureisSuccessful

HTTP 层

php 复制代码
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 流向应用再到领域,没有翻译的混乱。

数据库

sql 复制代码
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 语义,请明确区分。

澄清时间与时区

php 复制代码
$expiresAtUtc, $createdAt // 若默认使用 UTC

如需本地时区,请在命名中体现,或使用 ZonedDateTime 这类封装。

可选 vs 必填

避免使用 maybeopt 这类前缀;直接通过类型表达(?User?string)。

若需要三态布尔,用枚举或状态值(比如 consentStatus)替代 ?bool

临时变量

短暂的循环变量($i$line)可以接受,其余场景尽量给出有意义的命名。

"Manager / Helper" 气味

当你想写 SomethingManagerUtilHelper 时,考虑是否可以拆成更具体的小角色,如 TokenGeneratorSlugifierChecksumValidator

代码审查清单

审查或重命名时,可以自问:

  • 这个名字是否使用了业务团队熟悉的术语?
  • 作用域清晰吗(类 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...

前后对比库(快速获胜)

命名模糊的变量

php 复制代码
// 之前
$info = $service->get($id);

// 之后
$customerProfile = $profileService->fetchById($customerId);

泛化的函数

php 复制代码
// 之前
function process($a, $b, $c) { /* ... */ }

// 之后
function generateInvoiceFor(Order $order, TaxRules $rules, Currency $currency): Invoice { /* ... */ }

把副作用藏在 "get" 里

php 复制代码
// 之前
$report = $analytics->getMonthlyReport($month); // 会触发批处理

// 之后
$jobId = $analytics->scheduleMonthlyReport($month);

隐含单位

php 复制代码
// 之前
sleep($timeout);

// 之后
sleep($timeoutSeconds);

save 的多重含义

php 复制代码
// 之前
$orderRepository->save($order); // 插入?更新?还是 upsert?

// 之后
$orderRepository->add($order);     // 写入新订单
$orderRepository->update($order);  // 更新既有订单

与遗留代码和平相处

遇到遗留系统时,可考虑:

  • 在边界层引入适配器名称,内部逐步使用新命名。
  • 把第三方返回的原始数组包装成具名对象,靠近接口处完成转换。
  • 循序渐进地重命名,从常见方法着手,避免"大手术"。
  • 关键模块重命名前写批准测试(Golden Master)。

框架语境下的命名(Laravel、Symfony 等)

  • Laravel Eloquent :模型类默认单数(Order),表名复数(orders),顺势而为更省力。
  • Symfony :偏好显式服务和构造注入,按职责命名服务(SlugifierOrderNumberGenerator)让容器一目了然。
  • 事件/监听器:各框架略有差异,遵循框架约定可减少手工配置。

约定是一种"免费"的集成方式,善用它来降低仪式感。

常见命名陷阱

  • "Manager / Helper / Util" 大杂烩:通常意味着缺乏清晰抽象。
  • 匈牙利命名法 :2025 年不再需要 $strName;类型系统与 IDE 会帮你。
  • 模糊缩写:若必须使用,请文档化并保持一致(SKU、VAT、OTP)。
  • 过度前缀 :在 OrderService 内无需 orderServiceProcessOrder()
  • UI 驱动的命名BlueButtonHandler 在换皮后立刻过时。
  • 时间命名newOrdertempUserfinalData 很快就会说谎。

设立团队约定

命名确实包含品味,但团队共识可以降低摩擦:

  • 撰写简明的命名 ADR(Architecture Decision Record)。
  • 在仓库内提供示例 (如 /docs/naming.md)。
  • 在 PR 中鼓励提出具体替代方案 ("建议用 PaymentCaptureResult,因为它描述的是捕获结果而非 HTTP 响应")。
  • 定期复盘(例如季度),随着经验更新约定。

结语

好名字无法拯救糟糕的设计,却能让良好的设计发光,并降低未来重构的成本。PHP 给了你足够的灵活性,用命名把这种灵活性转化为清晰度。

从小处着手:重命名一个模糊变量,拆分一个泛泛的函数,引入一个值对象替换数组。持续迭代,几周之内你就能感受到代码库变得更轻、更清楚、更友好。

相关推荐
shencz200021 分钟前
在windows10里的apache2.4加php7.2的环境下上安装Laravel5.8框架
php·laravel
止语Lab27 分钟前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活27 分钟前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困32 分钟前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn33 分钟前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户2986985301439 分钟前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户772831049084041 分钟前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛1 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端