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。真正重要的不是背诵条文,而是在项目内部形成统一的执行标准。
变量:名词、单位与可信的布尔值
变量总是在描述某个对象、状态或集合。名称越贴近那个概念,后续阅读就越轻松。下面是几个值得坚持的做法:
用精确的名词
不好:
            
            
              php
              
              
            
          
          $items = $cart->get();更合适:
            
            
              php
              
              
            
          
          $cartItems = $cart->items();集合用复数,元素用单数
            
            
              php
              
              
            
          
          $users = $userRepository->findActive();
foreach ($users as $user) { /* ... */ }布尔值要像英文一样读
用 is、has、can、should、allows、supports 开头。
            
            
              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 通常隐含"即时""安全"的意味。如果方法需要访问外部服务或执行复杂逻辑,请改用 fetch、load、retrieve 等更准确的动词。
            
            
              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 替你打理结构
命名空间层级应与目录结构一一对应,这样逻辑分层更清楚,也能减少自动加载配置。
            
            
              css
              
              
            
          
          App\
  Checkout\
    Domain\
      Order\
      Cart\
      Payment\
    Application\
      PlaceOrder\
      RefundOrder\
    Infrastructure\
      Persistence\
      Http\这种布局让类名自带上下文:
            
            
              css
              
              
            
          
          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) {
        // 在这里验证格式
    }
}值对象既提供了语义明确的名字(如 Money、EmailAddress),也能在构造时守住不变量。
事件和异常:用后缀传达语境
事件
领域事件 通常用过去式(OrderPlacedEvent、PaymentCapturedEvent)。
应用或集成事件 倾向现在式或命令式(SendNewsletter、UserExportRequested)。
            
            
              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。选准映射方案,并且始终如一。
表和列
- 表名 :可以用单数或复数,但要统一(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.phpAPI 和 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 命令
语义要直接、面向任务:
            
            
              javascript
              
              
            
          
          php bin/console orders:rebuild-index
php bin/console users:import --from=legacy.csv除非领域内已有约定,否则少用 process、handle 这类泛词。
测试:写成可读的文档
测试面向的读者仅次于生产代码。命名清晰的测试在失败时会直接告诉你发生了什么。
类与文件名
测试类要镜像被测对象(SUT):OrderRepositoryTest、PlaceOrderHandlerTest。一对一是简单有效的默认规则。
方法名
任选以下风格之一,并保持一致:
BDD 风格
            
            
              php
              
              
            
          
          public function it_calculates_total_for_multiple_items(): voidGiven/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
- 命名空间通常镜像它,采用 PascalCase :Acme\PaymentGateway
- 描述务实准确 ;像 utils、helpers这种笼统名字会迅速过期。
安全地重构名字:技术债会发生
重命名是健康行为。几条常用守则:
- 使用 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;
}注意动词:nextId、add、fetchById、listByStatus。没有通用的 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(应用边界)、calculateTotal、capture、isSuccessful。
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 必填
避免使用 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...
前后对比库(快速获胜)
命名模糊的变量
            
            
              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 :偏好显式服务和构造注入,按职责命名服务(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 给了你足够的灵活性,用命名把这种灵活性转化为清晰度。
从小处着手:重命名一个模糊变量,拆分一个泛泛的函数,引入一个值对象替换数组。持续迭代,几周之内你就能感受到代码库变得更轻、更清楚、更友好。