PHP项目怎么实现分层架构检查?

wen java案例 1

本文目录导读:

PHP项目怎么实现分层架构检查?

  1. 方案一:基于命名空间与正则的静态扫描(最轻量,适合中小项目)
  2. 方案二:使用PHP-Parser进行AST静态分析(推荐,更准确)
  3. 方案三:使用Deptrac(专业级,适合大型项目)
  4. 方案四:运行时检查(AOP/代理模式,但不推荐用于生产)
  5. 建议与最佳实践
  6. 关键提醒

在PHP项目中实现分层架构检查,核心目标是防止跨层调用(例如Controller直接调用Repository,或Service层跳过Service直接操作Model)。

以下是几种从简单到复杂的实现方案,可以根据项目规模和严格程度选择。


基于命名空间与正则的静态扫描(最轻量,适合中小项目)

利用PHP的命名空间约定,写一个简单的脚本扫描所有PHP文件,检查不允许的跨层调用。

定义层级规则

  • Controller 只能调用 Service 或返回 Response
  • Service 只能调用 Repository 或其它 Service
  • Repository 只能调用 ModelDB 查询

实现检查脚本

<?php
// check_architecture.php
$rootDir = __DIR__ . '/app'; // 你的应用目录
$rules = [
    // 被调用者 => 允许的调用者列表
    '/Repository/' => ['/Service/', '/Console/', '/Command/'],
    '/Service/'    => ['/Controller/', '/Console/', '/Command/'],
    '/Model/'      => ['/Repository/'],
    '/Controller/' => ['Routes', 'Middleware'], // Controller 不应被业务层调用
];
$results = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootDir));
foreach ($iterator as $file) {
    if ($file->getExtension() !== 'php') continue;
    $content = file_get_contents($file->getPathname());
    $callerNamespace = getNamespaceFromFile($content); // 需要实现此函数
    // 检查是否调用了不允许的类
    foreach ($rules as $forbidden => $allowedCallers) {
        // 如果当前文件属于被禁止的层级
        if (preg_match($forbidden, $callerNamespace)) {
            // 检查调用者是否在白名单中
            $isAllowed = false;
            foreach ($allowedCallers as $pattern) {
                if (preg_match($pattern, $callerNamespace)) {
                    $isAllowed = true;
                    break;
                }
            }
            if (!$isAllowed) {
                $results[] = "违规: {$file->getPathname()} 中出现了不允许的调用";
            }
        }
    }
}
// 输出结果
if (empty($results)) {
    echo "✅ 架构检查通过\n";
} else {
    echo "❌ 发现 " . count($results) . " 处违规:\n";
    foreach ($results as $line) {
        echo "  - $line\n";
    }
    exit(1); // CI/CD 中可以阻断构建
}
function getNamespaceFromFile($content) {
    if (preg_match('/^namespace\s+(.+);/m', $content, $matches)) {
        return $matches[1];
    }
    return '';
}

优点:零依赖,可集成到Git Hooks/CI
缺点:只能检查字符串级别的调用,忽略动态调用、反射等


使用PHP-Parser进行AST静态分析(推荐,更准确)

利用nikic/php-parser解析PHP代码为抽象语法树(AST),精确识别use语句和方法调用。

安装依赖

composer require --dev nikic/php-parser

实现检查规则

<?php
// architecture_check.php
require 'vendor/autoload.php';
use PhpParser\{ParserFactory, NodeTraverser, NodeVisitorAbstract, Node};
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\MethodCall;
$projectDir = __DIR__ . '/src';
$violations = [];
// 定义层级映射和禁止规则
$layerMap = [
    'App\Controller' => 'controller',
    'App\Service'    => 'service',
    'App\Repository' => 'repository',
    'App\Model'      => 'model',
];
$forbiddenCalls = [
    'controller' => ['model', 'repository'], // Controller 禁止直接调用 Model / Repository
    'service'    => ['model'],                // Service 禁止直接调用 Model
    'repository' => [],                       // Repository 没有额外限制
];
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$traverser = new NodeTraverser();
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($projectDir));
foreach ($files as $file) {
    if ($file->getExtension() !== 'php') continue;
    $code = file_get_contents($file->getPathname());
    try {
        $ast = $parser->parse($code);
    } catch (Exception $e) {
        echo "解析错误: {$file->getPathname()} - {$e->getMessage()}\n";
        continue;
    }
    // 获取当前文件的命名空间和use语句
    $namespace = '';
    $uses = [];
    foreach ($ast as $stmt) {
        if ($stmt instanceof Node\Stmt\Namespace_) {
            $namespace = implode('\\', $stmt->name->parts);
        }
        if ($stmt instanceof Use_) {
            foreach ($stmt->uses as $use) {
                $uses[] = implode('\\', $use->name->parts);
            }
        }
    }
    // 确定当前文件的层级
    $currentLayer = null;
    foreach ($layerMap as $prefix => $layer) {
        if (strpos($namespace, $prefix) === 0) {
            $currentLayer = $layer;
            break;
        }
    }
    if (!$currentLayer) continue; // 跳过不匹配的目录
    // 遍历AST查找方法调用中的违规
    $traverser->addVisitor(new class($uses, $layerMap, $forbiddenCalls[$currentLayer] ?? [], $file->getPathname(), $namespace) extends NodeVisitorAbstract {
        private $uses, $layerMap, $forbiddenLayers, $filePath, $namespace;
        public function __construct($uses, $layerMap, $forbiddenLayers, $filePath, $namespace) {
            $this->uses = $uses;
            $this->layerMap = $layerMap;
            $this->forbiddenLayers = $forbiddenLayers;
            $this->filePath = $filePath;
            $this->namespace = $namespace;
        }
        public function enterNode(Node $node) {
            // 处理 new 关键字
            if ($node instanceof New_ && $node->class instanceof Node\Name) {
                $className = implode('\\', $node->class->parts);
                $this->checkForbiddenCall($className);
            }
            // 处理静态调用 Class::method()
            if ($node instanceof StaticCall && $node->class instanceof Node\Name) {
                $className = implode('\\', $node->class->parts);
                $this->checkForbiddenCall($className);
            }
        }
        private function checkForbiddenCall($className) {
            // 检查use语句中是否有违规调用
            foreach ($this->uses as $use) {
                if (strpos($use, $className) !== false) {
                    $resolvedClass = $use;
                }
            }
            if (!isset($resolvedClass)) return;
            // 确定被调用类的层级
            $calledLayer = null;
            foreach ($this->layerMap as $prefix => $layer) {
                if (strpos($resolvedClass, $prefix) === 0) {
                    $calledLayer = $layer;
                    break;
                }
            }
            if ($calledLayer && in_array($calledLayer, $this->forbiddenLayers)) {
                $violations[] = "违规: {$this->filePath} ({$this->namespace}) 调用了不允许的 {$resolvedClass}";
            }
        }
    });
    $traverser->traverse($ast);
}
if (empty($violations)) {
    echo "✅ 架构检查通过\n";
} else {
    foreach ($violations as $v) {
        echo "❌ $v\n";
    }
    exit(1);
}

优点:精确识别真实的类引用、忽略注释和字符串
缺点:无法处理动态变量调用 $class = 'App\Model\User'; new $class()


使用Deptrac(专业级,适合大型项目)

Deptrac 是一款专门用于PHP架构检查的工具,支持定义层、规则和排除项。

安装

composer require --dev qossmic/deptrac

配置规则 (deptrac.yaml)

# deptrac.yaml
parameters:
  layers:
    - name: Controller
      collectors:
        - type: className
          regex: .*Controller.*
    - name: Service
      collectors:
        - type: className
          regex: .*Service.*
    - name: Repository
      collectors:
        - type: className
          regex: .*Repository.*
    - name: Model
      collectors:
        - type: className
          regex: .*Model.*
  ruleset:
    Controller:
      - Service    # Controller 只能依赖 Service
    Service:
      - Repository # Service 只能依赖 Repository
    Repository:
      - Model      # Repository 只能依赖 Model
    Model:
      ~            # Model 不能依赖任何其他层
  skip_violations:
    App\SomeHelper: ~  # 跳过某些辅助类的检查

运行检查

./vendor/bin/deptrac analyze

输出示例:

[ERROR] Controller -> Model : App\Controller\UserController depends on App\Model\User (User.php:15)

优点

  • 成熟稳定,社区活跃
  • 支持YAML/XML/JSON配置
  • 可忽略特定违规、定义排除规则
  • 可与CI/CD无缝集成(exit code 1)

缺点:需要额外维护配置文件,小型项目可能显得重


运行时检查(AOP/代理模式,但不推荐用于生产)

通过动态代理或中间层,在运行时拦截调用并检查层关系。

class ServiceProxy {
    private $target;
    private $allowedCalls = ['Repository'];
    public function __call($method, $args) {
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        $caller = $trace[1]['class'] ?? '';
        // 检查调用者是否被允许
        $isValid = false;
        foreach ($this->allowedCalls as $prefix) {
            if (strpos($caller, $prefix) !== false) {
                $isValid = true;
                break;
            }
        }
        if (!$isValid) {
            throw new \RuntimeException("不允许从 $caller 调用此方法");
        }
        return $this->target->$method(...$args);
    }
}

问题

  • 严重性能损耗(每个请求都检查)
  • 依赖debug_backtrace,可能不准
  • 不适用于生产环境

仅限开发环境临时调试使用。


建议与最佳实践

项目规模 推荐方案 理由
小型(<10个模块) 方案一(正则扫描) 简单,5分钟搞定
中型(10-50个模块) 方案二(PHP-Parser) 准确,可定制
大型(>50个模块) 方案三(Deptrac) 专业、可维护、团队协作
任何规模 方案一 + Git Hook 在commit前自动检查

集成到CI/CD(以GitHub Actions为例):

# .github/workflows/architecture.yml
name: Architecture Check
on: [push, pull_request]
jobs:
  deptrac:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
      - run: composer install --dev
      - run: ./vendor/bin/deptrac analyze

关键提醒

  1. 不要过度设计:分层检查是为了防止低级错误,不是限制合理的设计模式(如Repository可以直接调用其他Repository,如果业务逻辑允许)。
  2. 允许例外:使用skip_violations或白名单机制,处理必要的跨层调用(例如Helper类、事件监听器)。
  3. 保持配置文件与代码同步:如果重构了目录结构,记得更新检查规则。

最终建议:对于大多数PHP团队,直接从Deptrac开始是最经济的选择——它在社区验证过、文档完善、易于与CI集成,能让你把精力放在业务代码上,而不是造一个轮子来检查架构。

抱歉,评论功能暂时关闭!