PHP 工业化研发交付标准体系

  下面这套是我按"工业化"思路整理的——目标不是"能跑就行",而是任何一个新人接手任何一个项目,都能用同一套规矩、同一套工具、──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  同一套验收标准交付。每节都给完整代码 + 大白话。

  ---
  0. 全景图(先把"工业化"拆清楚)

  需求    设计     编码      接口      构建      测试      发布      验收      运行
   │      │       │        │        │        │        │        │        │
   ▼     ▼      ▼       ▼       ▼       ▼       ▼       ▼       ▼
  PRD →ADR/RFC →PSR/Lint →OpenAPI →CI/CD →单//E2E →灰度 →DoD/UAT →SLO
                                ↑                                         │
                                └── Schema 即契约,前后端 / 服务间共同遵守 ┘

  人话:每一格都有"输入物 + 产出物 + 谁验收",不靠人盯靠工具盯。下面一格一格落地。

  ---
  1. 项目骨架:所有项目长一个样

  acme-service/
  ├── app/                    # 业务代码(领域分层)
  │   ├── Domain/             # 领域模型(纯 PHP,无框架)
  │   ├── Application/        # 用例 / Service
  │   ├── Infrastructure/     # DB / HTTP / MQ / Cache 适配
  │   └── Interface/          # Http / Cli / Rpc 入口
  ├── config/                 # 配置(按环境分文件)
  ├── database/migrations/    # 数据库变更(不可逆操作单独标记)
  ├── docs/
  │   ├── adr/                # 架构决策记录
  │   ├── api/openapi.yaml    # 接口契约(唯一真源)
  │   └── runbook/            # 运维手册
  ├── tests/{Unit,Feature,Contract,E2E}/
  ├── .github/workflows/      # CI
  ├── deploy/                 # Dockerfile / k8s / helm
  ├── tools/                  # 一次性脚本、生成器
  ├── composer.json
  ├── phpstan.neon  psalm.xml  .php-cs-fixer.php  rector.php
  ├── Makefile                # 统一入口
  └── README.md

  人话:骨架统一以后,每个新仓库 git clone 下来 make up 就能跑、make ci 就能验收。下面给 Makefile:

  .PHONY: up down ci lint stan psalm test cs fix migrate seed openapi

  up:        ; docker compose up -d --wait
  down:      ; docker compose down -v
  install:   ; composer install --prefer-dist --no-interaction
  cs:        ; vendor/bin/php-cs-fixer fix --dry-run --diff
  fix:       ; vendor/bin/php-cs-fixer fix && vendor/bin/rector process
  stan:      ; vendor/bin/phpstan analyse --memory-limit=1G
  psalm:     ; vendor/bin/psalm --no-cache
  test:      ; vendor/bin/phpunit --testdox
  contract:  ; vendor/bin/phpunit --testsuite=Contract
  openapi:   ; vendor/bin/openapi app -o docs/api/openapi.yaml && npx @stoplight/spectral-cli lint docs/api/openapi.yaml
  migrate:   ; php artisan migrate --force
  ci:        cs stan psalm test contract openapi

  ---
  2. 编码规范:PSR + Lint + 自动修复

  2.1 composer.json(标准依赖与脚本)

  {
    "name": "acme/service",
    "type": "project",
    "require": { "php": "^8.3" },
    "require-dev": {
      "phpunit/phpunit": "^11",
      "phpstan/phpstan": "^1.11",
      "phpstan/phpstan-strict-rules": "^1.5",
      "vimeo/psalm": "^5",
      "friendsofphp/php-cs-fixer": "^3.59",
      "rector/rector": "^1.2",
      "infection/infection": "^0.29",
      "roave/security-advisories": "dev-latest",
      "zircote/swagger-php": "^4",
      "pestphp/pest": "^2"
    },
    "scripts": {
      "ci": ["@cs","@stan","@psalm","@test","@contract"],
      "cs":   "php-cs-fixer fix --dry-run --diff",
      "stan": "phpstan analyse",
      "psalm":"psalm",
      "test": "phpunit",
      "contract":"phpunit --testsuite=Contract"
    },
    "config": { "sort-packages": true, "allow-plugins": { "*": true } }
  }

  2.2 .php-cs-fixer.php(PSR-12 + 团队约定)

  <?php
  return (new PhpCsFixer\Config())
      ->setRiskyAllowed(true)
      ->setRules([
          '@PSR12' => true,
          '@PHP83Migration' => true,
          'declare_strict_types' => true,
          'array_syntax' => ['syntax' => 'short'],
          'ordered_imports' => ['sort_algorithm' => 'alpha'],
          'no_unused_imports' => true,
          'single_quote' => true,
          'native_function_invocation' => ['include' => ['@all']],
          'global_namespace_import' => ['import_classes' => true, 'import_functions' => false],
      ])
      ->setFinder(PhpCsFixer\Finder::create()->in([__DIR__.'/app', __DIR__.'/tests']));

  2.3 phpstan.neon(最严级别 9)

  parameters:
    level: 9
    paths: [app, tests]
    treatPhpDocTypesAsCertain: false
    checkUninitializedProperties: true
    checkMissingIterableValueType: true
    reportUnmatchedIgnoredErrors: true
  includes:
    - vendor/phpstan/phpstan-strict-rules/rules.neon

  人话:@PSR12 + level 9 + strict_types + Psalm 是 PHP 工业化的"四件套"。CI
  里这四样不绿,代码不准合并。这条规矩立住,整个团队的代码风格就齐了。

  ---
  3. 分层架构:六边形(端口适配器)

  <?php
  // app/Domain/Order/Order.php ——纯领域,无框架依赖
  declare(strict_types=1);
  namespace App\Domain\Order;

  final class Order
  {
      public function __construct(
          public readonly OrderId $id,
          public readonly UserId  $userId,
          public Money            $amount,
          public OrderStatus      $status,
      ) {}

      public function pay(Money $paid): void
      {
          if ($this->status !== OrderStatus::Pending) {
              throw new DomainException('only pending order can be paid');
          }
          if ($paid->lt($this->amount)) {
              throw new DomainException('insufficient payment');
          }
          $this->status = OrderStatus::Paid;
      }
  }

  <?php
  // app/Application/Order/PayOrderUseCase.php ——用例:编排
  final class PayOrderUseCase
  {
      public function __construct(
          private OrderRepository $repo,      // 端口(接口)
          private EventBus        $bus,
          private TransactionRunner $tx,
      ) {}

      public function execute(PayOrderCommand $cmd): OrderId
      {
          return $this->tx->run(function () use ($cmd) {
              $order = $this->repo->findOrFail($cmd->orderId);
              $order->pay($cmd->amount);
              $this->repo->save($order);
              $this->bus->dispatch(new OrderPaid($order->id));
              return $order->id;
          });
      }
  }

  <?php
  // app/Infrastructure/Persistence/PdoOrderRepository.php ——适配器
  final class PdoOrderRepository implements OrderRepository
  {
      public function __construct(private \PDO $db) {}
      public function save(Order $o): void { /* PDO 预编译 */ }
      public function findOrFail(OrderId $id): Order { /* ... */ }
  }

  人话:领域不知道数据库存在,用例不知道 HTTP 存在。换 MySQL 换 PG、换 HTTP 换
  gRPC,业务代码一行不动。这是工业化最值钱的一条架构纪律。

  ---
  4. 接口契约:OpenAPI 是唯一真源

  4.1 用注解写,自动生成 openapi.yaml

  <?php
  // app/Interface/Http/OrderController.php
  use OpenApi\Attributes as OA;

  #[OA\Info(version: '1.0.0', title: 'Order API')]
  final class OrderController
  {
      #[OA\Post(
          path: '/api/v1/orders/{id}/pay',
          summary: '支付订单',
          tags: ['Order'],
          requestBody: new OA\RequestBody(
              required: true,
              content: new OA\JsonContent(ref: '#/components/schemas/PayOrderRequest')
          ),
          responses: [
              new OA\Response(response: 200, description: 'OK',
                  content: new OA\JsonContent(ref: '#/components/schemas/OrderView')),
              new OA\Response(response: 409, description: 'Conflict',
                  content: new OA\JsonContent(ref: '#/components/schemas/Problem')),
          ]
      )]
      public function pay(string $id, PayOrderRequest $req): OrderView { /* ... */ }
  }

  #[OA\Schema()]
  final class PayOrderRequest {
      #[OA\Property(format: 'decimal', example: '99.00')] public string $amount;
      #[OA\Property(example: 'CNY')]                       public string $currency;
  }

更多推荐