本文目录导读:

- 方案一:基于命名空间与正则的静态扫描(最轻量,适合中小项目)
- 方案二:使用PHP-Parser进行AST静态分析(推荐,更准确)
- 方案三:使用Deptrac(专业级,适合大型项目)
- 方案四:运行时检查(AOP/代理模式,但不推荐用于生产)
- 建议与最佳实践
- 关键提醒
在PHP项目中实现分层架构检查,核心目标是防止跨层调用(例如Controller直接调用Repository,或Service层跳过Service直接操作Model)。
以下是几种从简单到复杂的实现方案,可以根据项目规模和严格程度选择。
基于命名空间与正则的静态扫描(最轻量,适合中小项目)
利用PHP的命名空间约定,写一个简单的脚本扫描所有PHP文件,检查不允许的跨层调用。
定义层级规则
Controller只能调用Service或返回ResponseService只能调用Repository或其它ServiceRepository只能调用Model或DB查询
实现检查脚本
<?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
关键提醒
- 不要过度设计:分层检查是为了防止低级错误,不是限制合理的设计模式(如Repository可以直接调用其他Repository,如果业务逻辑允许)。
- 允许例外:使用
skip_violations或白名单机制,处理必要的跨层调用(例如Helper类、事件监听器)。 - 保持配置文件与代码同步:如果重构了目录结构,记得更新检查规则。
最终建议:对于大多数PHP团队,直接从Deptrac开始是最经济的选择——它在社区验证过、文档完善、易于与CI集成,能让你把精力放在业务代码上,而不是造一个轮子来检查架构。