关于 phpgraph
phpgraph 对 PHP 项目进行静态分析,为编码 Agent 构建代码知识图谱。它以带置信度标签的事实映射调用、路由、消息、依赖和架构,不执行代码,也不会将代码发送到本机之外。
主要功能
- 01通过类型推断构建调用图
- 02映射路由、消息和处理器
- 03追踪依赖与变更影响
- 04检查架构层和规则
- 05关联 API 模式与实现
- 06报告未解析关系和分析盲区
适用场景
- 01了解陌生的 PHP 项目
- 02追踪请求从路由到处理器的路径
- 03变更前识别受影响代码和测试
- 04在 CI 中检查架构层规则
MCP 相关
通过 MCP 提供静态分析结果,包含项目概览、图查询、代码结构、节点关系、影响分析和路径查询等工具。图谱在首次使用时构建,并在代码变化时增量更新。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm ghcr.io/hbenabdallah/phpgraph:1.6.1本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/hbenabdallah/phpgraph:1.6.1stdio工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
phpgraph
为 AI 编码 Agent 打造的 PHP 代码库知识图谱。
通过 MCP 向 Sherpa、Claude Code、Codex、Cursor 和任何 MCP 客户端提供静态分析结果。
无需大模型、网络或嵌入:提供确定性事实,并为每项事实标注置信度。
English · Français
为什么选择 phpgraph?
AI Agent 遇到不熟悉的 PHP 项目时,会逐个读取文件并搜索名称。但它会错过真实代码库中最重要的信息:谁调用了什么、哪位处理器接收消息、哪个控制器处理路由、方法更改会造成什么影响,以及各层之间的依赖关系。
phpgraph 使用 nikic/php-parser 解析整个项目(使用 phpstan/phpdoc-parser 解析文档块),然后通过图谱回答这些问题:
- 提供确定性事实,而非猜测。 每种关系都会标记为
EXTRACTED(从代码中读取)、INFERRED(根据声明的类型解析)或AMBIGUOUS(猜测,并明确标注)。 - 明确说明未知之处。 未解析的调用、缺失的
vendor/、没有处理器的消息:这些缺漏都会报告出来,让 Agent 知道何时需要自行阅读代码。 - 专为 DDD 和六边形架构打造,随后扩展到微服务,再到一般 PHP 项目:Symfony、Laravel、Ecotone、PrestaShop、WordPress 或纯 PHP。
- 从设计上保护隐私。 你的代码绝不会离开本机,phpgraph 也绝不会执行代码,只会解析代码。
功能
- 带类型推断的调用图:返回类型、链式调用(
$a->b()->c())、局部变量、文档块(@return、@var),以及vendor/依赖的签名,让调用链可以贯穿 Doctrine、Symfony 或 Laravel API。 - 架构:DDD 和六边形分层、限界上下文、在 CI 中检查分层规则(
phpgraph check),以及影响分析(impact_of):哪些内容依赖某个类或方法,以及要运行哪些测试。 - 消息和事件:Symfony Messenger、Laravel 作业和事件、Ecotone、PrestaShop CQRS、WordPress 钩子、领域事件和自定义总线的发送方 → 消息 → 处理器关系,来源可以是属性、服务配置(YAML、XML、PHP)或代码结构。
- HTTP:将路由(Symfony 属性、YAML 和 PHP 路由文件、API Platform 资源、Laravel 路由文件、以容器服务 ID 命名的控制器)关联到控制器,并将 HTTP 调用关联到其访问的路由。如果某个路由的控制器类既不在项目中,也不在
vendor/中,报告中会指出其路由文件。 - 依赖注入:Symfony 容器配置除构造函数类型之外注入的内容、某个标签的所有服务(
tagged_iterator、#[AutowireIterator])或按 ID 命名的服务,支持从 PHP、YAML 或 XML 配置中解析;也支持带字面量参数调用的辅助方法(Wiring::wire($services, 'sales', ...),只求值,不执行):验证器会关联到它收到的规则。声明自身可处理哪些对象的规则(supports(object $input): bool { return $input instanceof CreateEstimateQuery; },也包括canHandle、accepts等)不会被传入其他输入的用例关联到共享流水线中,输入属性所持有的类也包括在内。接口类型的输入可能是任何实现该接口的项目类,以及输入被传递给的参数类型(将QueryInterface $query传给setUp(CreateEstimateQueryInterface $query):一个CreateEstimateQuery);无法读取的守卫,或仅针对其中某些类运行的规则,会保留并标记为supports() not resolved。 - 微服务:每个服务使用独立的 ID 空间,并通过各自的契约连接服务:共享消息类、路由键、HTTP 路由。
- API 架构:将 OpenAPI(Swagger)文档中的操作关联到实现它们的路由;如果文档描述的是其他 API,则关联到其客户端的 HTTP 调用;将 protobuf 服务的 RPC 关联到实现其生成接口的类。报告会指出已记录但未实现的内容,以及没有文档描述的路由。
impact会列出更改所涉及的操作(客户端依赖的契约),outline会列出某个功能所服务的操作。 - 零配置 MCP 服务:首次使用时构建,代码更改时增量重建(在含 8,400 个文件的项目中,每次编辑约需 3 秒)。
快速开始
1. 安装。 单个可执行文件,无需 PHP(支持 Linux 和 macOS 的 x86_64 与 ARM,以及 Windows 的 x86_64):
curl -fsSL https://raw.githubusercontent.com/hbenabdallah/phpgraph/main/install.sh | sh # latest release
curl -fsSL https://raw.githubusercontent.com/hbenabdallah/phpgraph/main/install.sh | sh -s -- 0.2.0 # a given release
curl -fsSL https://raw.githubusercontent.com/hbenabdallah/phpgraph/main/install.sh | sh -s -- dev-main # the main branch
irm https://raw.githubusercontent.com/hbenabdallah/phpgraph/main/install.ps1 | iex # Windows
$env:PHPGRAPH_VERSION = "dev-main"; irm https://raw.githubusercontent.com/hbenabdallah/phpgraph/main/install.ps1 | iex
该脚本会下载二进制文件、校验其 SHA-256,并将其放入 ~/.local/bin(Windows:%LOCALAPPDATA%\Programs\phpgraph,并添加到 PATH);设置 PHPGRAPH_INSTALL_DIR 可更改目录。再次运行即可更新。每次推送到 main 时,都会重新构建 dev-main(edge 预发布版本)。该二进制文件是内置 phpgraph 的静态 PHP 运行时,使用 static-php-cli 构建。
已安装 PHP 8.2+,或使用 Docker 时:
composer require --dev hbenabdallah/phpgraph # or: composer global require hbenabdallah/phpgraph
curl -LO https://github.com/hbenabdallah/phpgraph/releases/latest/download/phpgraph.phar
docker pull ghcr.io/hbenabdallah/phpgraph # nothing else to install
2. 连接你的 Agent。 mcp-config 会打印一份可直接粘贴的配置,并填入路径(通过 Composer 安装时为 vendor/bin/phpgraph):
phpgraph mcp-config sherpa # Sherpa: a `sherpa mcp add` command, one declaration for every project
phpgraph mcp-config claude # Claude Code: a `claude mcp add` command, or .mcp.json
phpgraph mcp-config codex # Codex: ~/.codex/config.toml
phpgraph mcp-config cursor # Cursor: .cursor/mcp.json, shareable with the team
phpgraph mcp-config json # any other MCP client over stdio
即使已连接 phpgraph,Agent 还是会先用 grep。mcp-config 还会打印一行内容,供你添加到项目的说明文件(CLAUDE.md、AGENTS.md)中,告知 Agent 在使用 grep 或读取文件之前先调用 phpgraph;服务的 MCP 说明中也有同样的要求。
使用终端编码 Agent Sherpa 时,只需声明一次即可用于所有项目:Sherpa 会从其运行所在的项目启动 MCP 服务。
sherpa mcp add phpgraph -- ~/.local/bin/phpgraph serve
在 Sherpa 会话中,/mcp add 也能完成同样的操作,工具随即即可使用。两种方式都会写入 ~/.config/sherpa/mcp.json。
使用 Claude Code 时(使用绝对路径,与 mcp-config 打印的路径一致):
claude mcp add phpgraph -- /home/you/.local/bin/phpgraph serve /path/to/project
3. 向 Agent 提问,例如 “概述一下这个项目”、“订单是如何下单的?” 或 “如果我更改 OrderRepository::save,会有什么影响?”。首次调用时会构建图谱,之后会持续保持最新。
MCP 工具
| 工具 | 用途 |
|---|---|
overview | 从这里开始:Composer 技术栈、包含各层的命名空间树、有界上下文、层规则违规、消息、HTTP 路由(每条包含方法、路径、控制器和文件,最多 30 条;超出则按路径前缀分组)、服务,以及图谱无法看到的内容。 |
query_graph | 当你不知道类名时,通过类名中的词查找某个主题的相关代码(例如,验证器会匹配到 Validator),并显示它所命名类周围的功能类。关于路由的问题("mooc courses routes"、"GET /courses")会返回匹配的路由及其控制器。答案周围的类群按文件夹计数(ContextRuleInterface: 34 in …),注入的列表只计一次(ContextValidator receives 34 ContextRuleInterface)。 |
outline | 解释某项功能前,先查看其结构大纲,以便只阅读少数包含不明显逻辑的文件。内容包括:按命名空间和层列出的组成类,以及其文档块的首句、公开签名、常量和枚举项;周围的接口和基类及其按模块列出的实现(0 表示已说明);逐条调用链中到达该功能的路由,包括它运行的闭包;从方法体读取的行为(受保护的抛出、提前返回、基于常量的分支、循环上限、前后状态比较);包含服务 ID 的容器装配;该功能的使用者、按模块统计的测试、没有测试涉及的内容,以及没有任何代码使用的内容。约 15 KB;如果列表被截短,会指出包含全部内容的章节(section behaviour,1 至 9 KB)。如果传入一个方法(OrderService::place),则改为展示它运行的内容:它调用的方法,最多深入 3 次调用,每个类只列出这些方法;统计枢纽(被 20 个或更多类调用的类,以及单例的 getInstance()),但不继续追踪;按从近到远的顺序展示其行为。 |
get_node | 查看一个类、方法、路由或频道及其所有关系;按模块统计使用它的测试。 |
get_neighbors | 查看使用某节点的内容(in),或该节点依赖的内容(out)。 |
impact_of | 修改前查看:每个依赖某个类或方法的类,按从近到远的顺序排列,并附上调用行;通过其实现接口的调用者;依赖其写入状态的代码;到达它的路由及每条调用链;以及通过测试辅助工具找到的待运行测试。它会标记没有测试涉及的类,以及应用中没有任何代码使用的类。 |
shortest_path | 查看两段代码如何关联。 |
god_nodes | 查找大部分代码所依赖的枢纽。 |
服务会告知 Agent 首先调用 overview。在 php-ddd-example 上,Agent 看到的路径如下:
Shortest path (4 hops):
PUT /courses/{id} --handled_by--> CoursesPutController::__invoke()
--dispatches--> CreateCourseCommand --handled_by--> CreateCourseCommandHandler::__invoke()
--calls--> CourseCreator::__invoke()
命令行
phpgraph build [path] # phpgraph-out/graph.json and GRAPH_REPORT.md
phpgraph overview # stack, structure and gaps
phpgraph impact "OrderRepository::save" # what depends on it, and the tests to run (--all, --section tests, --format json)
phpgraph check # layer rules, for CI: exit code 1 on a new violation
phpgraph explain "PlaceOrderHandler" # a node and its relations (-d in|out)
phpgraph path "StockChecker" "DbalOrderRepository"
phpgraph query "how is stock checked"
phpgraph outline "notification validation" # the outline of a feature (--section flow, --format json)
phpgraph serve [path] # the MCP server, over stdio
phpgraph mcp-config claude|codex|cursor|json [--docker image]
build 选项:-e 用于排除路径(可重复指定),--no-vendor 表示不读取依赖项,--no-cache 表示重新解析每个文件。vendor、node_modules、var、.git 以及项目自身 .gitignore 文件中忽略的文件都会被跳过;项目上层的 Git 仓库不会应用其规则,因此复制到忽略目录中的项目仍会被读取。当目录中有 PHP 文件,但全部被排除时,build 会失败并说明原因;每个 MCP 答案都会以该警告开头,而不是提供空图谱。默认情况下内存不设上限;PHPGRAPH_MEMORY_LIMIT=2G 可设置上限。安装 PHP 后,opcache 及其 JIT 可使构建速度提高约 20%:php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=128M vendor/bin/phpgraph build(Docker 镜像和 mcp-config 打印的命令已启用这些选项)。
图谱包含的内容
| 关系 | 来源 → 目标 |
|---|---|
defines、imports | 文件 → 类、use 语句 |
extends、implements、uses_trait | 类 → 父类、接口、trait |
has_method、overrides | 类 → 方法;方法 → 它重写的方法 |
instantiates、references | new Foo();参数、返回值和属性类型,catch、instanceof、常量、属性 |
calls | 方法 → 解析出的被调用方法,并标注调用位置的行号;运行所接收闭包的方法($apply(...))会调用该闭包所调用的方法,INFERRED,通过 via 标明 closure of <the method writing it> |
dispatches、handled_by | 发送方 → 消息或通道 → 处理程序;路由 → 控制器;由 supports() 方法测试的类 → 对应策略 |
contract | 一个服务发送的消息类 → 另一个服务处理的同一类 |
requests | HTTP 调用 → 它访问的路由(可以在同一服务或另一服务中),或另一 API 的 OpenAPI 文档中的操作 |
implemented_by | API schema 操作(operation:)→ 对应方法和路径的路由(OpenAPI),或实现生成服务的类的方法(protobuf) |
receives | 服务 → 容器配置注入其中的每个服务(标签、ID 或被装饰的服务);via 用于说明具体是哪一个:当一个类有多个服务时,使用 service sales.pipeline |
reads_state_of | 方法 → 同一类中修改其读取属性的方法,构造函数之外(hasErrors() → add()),INFERRED |
每种关系都带有一个置信级别:
| 级别 | 含义 |
|---|---|
EXTRACTED | 在单个文件的代码或配置(属性、服务标签、路由文件)中直接读取。 |
INFERRED | 根据声明的类型跨文件解析:参数、属性、$this、继承、返回类型、instanceof 测试、代码结构。如果分支结束后某个变量可能持有多个类的实例,则将调用关联到每个类的方法。 |
AMBIGUOUS | 一种猜测,例如项目中唯一一个同名方法,但接收对象类型未知;或者变量可能持有某个类的实例,但另一个分支结束后其类型未知。 |
识别的框架和约定
| 处理程序和监听器 | 发送的消息 | HTTP | |
|---|---|---|---|
| Symfony | #[AsMessageHandler]、#[AsEventListener]、标签 messenger.message_handler 和 kernel.event_listener(YAML、XML、PHP、_instanceof)、getSubscribedEvents()、addListener() | MessageBusInterface、EventDispatcherInterface、命名事件 | #[Route](路径也可定义在类常量中)、YAML 和 PHP 路由文件、由服务 ID 命名的控制器;API Platform #[ApiResource] 和操作属性,由其 provider、processor 或控制器处理 |
| Laravel | $listen、Event::listen()、订阅者(subscribe() 中的 $events->listen(),或其返回的映射)、联合事件的监听器(handle(Accepted|Rejected $event))、作业(ShouldQueue、Dispatchable) | event()、dispatch()、facade、Job::dispatch() | Route::get/post/…、match、resource、view、redirect、带 prefix 和 domain 的分组、routes/api.php;Http:: 客户端,相对于其 baseUrl() |
| Ecotone | #[CommandHandler]、#[EventHandler]、#[QueryHandler]、路由键 | CommandBus、EventBus、DistributedBus、sendWithRouting() | |
| PrestaShop | #[AsCommandHandler]、#[AsQueryHandler] | CommandBusInterface::handle() | YAML 路由 |
| WordPress | add_action()、add_filter() | do_action()、apply_filters() | |
| 自定义代码 | 可按名称识别的处理程序属性和标签、__invoke/handle(Message $m)、*Handler 类 | *Bus、*Dispatcher、*Publisher 类型、聚合的 recordThat()、自定义总线封装器 | Guzzle、Symfony HttpClient、PSR-18 |
架构:分层、有界上下文、CI 中的规则
类的层级从其命名空间中读取:第一个显式层级段(`Domain`、`Application`、`Infrastructure`、`UI`、`Presentation`、`Port`、`Adapter`、`UseCase`);若没有,则读取最后一个通用层级段(`Model`、`Entity`、`Controller`、`Http`、`Persistence` 等)。限界上下文从层级之前读取(`CodelyTv\Mooc\Courses\Domain`),或者当项目将层级放在最前面时从其之后读取(`Core\Domain\Product`)。phpgraph check 会将分层架构的默认规则应用于应用代码:domain 不得依赖 application、infrastructure 或 interface;application 和 port 不得依赖 infrastructure 或 interface。对于已有违规项的项目:
phpgraph check --generate-baseline # accept today's violations in phpgraph-baseline.json
phpgraph check # fail on new ones only
项目自身的规则写在 phpgraph.yaml 中,并由同一命令使用同一基线检查。模式是类名上的 glob(* 匹配一个命名空间段内的内容,** 跨段匹配),以 \ 结尾的命名空间包含其中的所有内容:
rules:
dependencies: # who may depend on what
- name: doctrine-in-infrastructure
dependency: 'Doctrine\'
allow-from: ['App\*\Infrastructure\'] # or deny-from, which wins
reason: Only infrastructure talks to the database.
structure: # what the classes a pattern selects must be
- on: 'App\**\Controller\*'
not-on: 'App\Legacy\**'
target: class # class, interface, trait or enum
must-be-final: true # must-be-abstract, must-be-readonly: true or false
must-be-named: '*Controller'
must-implement: ['App\Shared\Http\Controller'] # must-extend, must-use-trait
only-public-methods: [__construct, __invoke]
handlers: # a message handler dispatches the messages of its own bounded context
own-context: true
shared: [Shared] # contexts any handler may use
依赖仅包括应用代码中确定的依赖(EXTRACTED 和 INFERRED:类型、属性、new、调用、继承);继承关系从图中读取,因此无法获知 vendor/ 中类的父类。处理器和消息的限界上下文与层级一样,都是从其命名空间中读取。
微服务
包含两个或更多独立 Composer 应用的代码库会被拆分为多个服务。单体仓库中的包仍属于其项目:根目录自动加载、声明为路径仓库或通过 replace 替换的包是项目的一部分,而不是服务;服务所需的库(路径仓库或其包名)则是它们的共享代码。此时,ID 会带上所属服务,例如 billing@App\Domain\Order,名称绝不会解析到其他服务:服务通过契约交互。
- 一个服务发送、另一个服务处理的同一消息类:
contract; - 路由键(
sendWithRouting('order.place', …)、#[CommandHandler('ticket.create')]、具名事件):所有服务共享的channel:节点; - 对另一服务路由的 HTTP 调用:
requests。如果调用路径只匹配为其他 HTTP 方法声明的路由,则会报告该调用:按当前写法,它会失败; - API 架构:OpenAPI 文档(
GET /api/hotels/{hotel})或 protobuf 服务(CinemaService.Reserve)中的operation:节点,由提供这些操作的路由或类通过implemented_by关联。代码库根目录中的文档描述所有服务(即 API 网关),服务目录中的文档则只描述该服务。
当自动检测不适用时,可选的 phpgraph.yaml 可用于指定服务:
services:
- services/billing
- services/shipping
自定义路由文件:XML 中的操作代码
旧版应用通常通过屏幕发送的操作代码(value="saveReservation")进行路由,并由自定义 XML 文件将其映射到控制器方法。phpgraph.yaml 用于描述格式,不会硬编码任何内容:
actions:
- file: Framework/jacActions.xml
element: //action # XPath of the action elements
code: code # their attributes (these are the defaults)
controller: controller
method: method
controller_file: '{dir}/control/{name}CTRL.php' # _Stock/Reservation → _Stock/control/ReservationCTRL.php
target: target # the action or template it goes on to
forward: { element: forward, result: result, target: target }
cited_in: [html, js] # the screens quoting the codes
每个操作都是一个路由节点,例如 ACTION saveReservation,通过 handled_by 关联到其方法(EXTRACTED);如果方法继承而来,则通过 via controller 引用其控制器。其目标和转发通过 references 指向下一个操作或模板(_Stock/SO_080/Resa.html 会查找 _Stock/views/SO_080/Resa.html),并通过 via target 或 forward KO 标记。引用该代码(value="saveReservation"、'saveReservation&id=')的 cited_in 文件会指向该操作,且状态为 AMBIGUOUS:impact_of 会在路由下列出这些屏幕。对于普通单词形式的代码(default、login),不会进行查找;压缩文件也不会查找。overview 会统计操作数量,并列出缺少控制器或方法的操作;方法的 outline 会从其操作开始展示流程;query_graph("saveReservation routes") 可以找到该操作。controller_class: '{name}CTRL' 会按类名而不是文件查找类。
工作原理
1. 每个 PHP 文件都会被解析为事实:声明、调用(包括描述接收者的表达式类型)、消息发送和处理器、路由、HTTP 调用。 2. 构建器会跨文件解析这些事实:通过继承和返回类型确定类型,按需读取 Composer 元数据中的 `vendor/` 签名(只解析,绝不执行),并从容器配置中读取服务定义。 3. 图会保存为 `phpgraph-out/graph.json`,并生成供人阅读的 `GRAPH_REPORT.md`。重建采用增量方式:按内容缓存文件的提取结果;如果没有任何声明发生变化,MCP 服务只会重新解析已更改文件中的调用。结果与完整构建完全一致。
在真实项目上的测量结果
phpgraph 基于一组固定到特定提交的开源项目进行开发,并通过这些项目的指标评估每项改动(composer corpus:measure)。仅统计应用代码;接收者类型已确定表示 phpgraph 能确定接收者类型的方法调用所占的比例。构建数据由 corpus:measure 测得,每次在 Docker 中测量一个项目(PHP 8.5,不启用 opcache),版本为 1.5.8。
| 项目 | 类型 | PHP 文件数 | 构建时间 | 接收者类型已确定 |
|---|---|---|---|---|
| php-ddd-example | DDD,CQRS | 304 | 0.8 s | 99.1 % |
| Sylius | Symfony 电商 | 4,946 | 12.7 s | 98.6 % |
| Akeneo PIM | 六边形架构、有界上下文 | 8,416 | 18.7 s | 92.9 % |
| PrestaShop | CQRS 和遗留代码 | 7,850 | 20.4 s | 96.5 % |
| Ecotone quickstart | 47 个服务、消息传递 | 583 | 1.1 s | 92.7 % |
| BookStack | Laravel | 1,513 | 6.6 s | 97.4 % |
| WordPress | 无框架、钩子 | 1,899 | 9.7 s | 97.7 % |
语料库还包含四个微服务项目:两个通过 RabbitMQ 通信的 Laravel 服务,由 4 个消息契约关联;四个通过 HTTP 通信的 Laravel 服务,phpgraph 在其中发现了一个发送到仅支持 GET 的路由的 POST 请求;Spiral 的票务预订项目,服务通过 .proto 文件中声明的 gRPC 服务相互调用;以及 StayHub,七个 Laravel 服务位于由 OpenAPI 文件描述的 API 网关之后。后两个项目将 API 架构作为契约进行测量:StayHub 文档中的全部 45 个操作,以及 ticket-booking 中的全部 14 个 rpc,都关联到了其实现代码。约 26,500 个文件中没有解析失败。各项目的详细信息(包括消息、路由和服务)见 corpus/RESULTS.md。
与 Agent 配合使用
在一个包含 4,800 个文件的私有 Symfony/DDD 项目上测量,每种设置运行 3 次;使用相同的 Agent(Sherpa),分别测试不使用和使用 phpgraph 1.0.2 的情况:
| 问题类型 | 使用 phpgraph | 质量 |
|---|---|---|
影响分析或重构(impact_of) | 令牌数减少 42 %,耗时减少 50 %,工具调用次数减少 59 % | 相同或更好 |
解释功能(outline) | 令牌数减少 25 %,耗时减少 34 %,工具调用次数减少 49 %;使用默认 outline 时令牌数减少 31 % | 相同 |
使用和不使用 phpgraph 时,各次运行的结果范围互不重叠。
与其他代码图的对比
Kimai 2.70.2(Symfony,1,072 个源文件和 761 个测试文件,不在 phpgraph 的测量语料库中),2026 年 10 月,不使用任何 LLM:phpgraph 1.5.2 和 graphify 0.9.93(extract --code-only),在同一台机器上,通过 Docker 运行。graphify 为每个调用方和被调用方保留一条边,因此按方法对比较项目方法之间的调用。差异样本已在代码中核查。tools/graphify/benchmark.sh 可再次运行基准测试。
| phpgraph | graphify | |
|---|---|---|
| 构建(6 次运行的中位数;graphify 每次都慢 2.2 到 2.7 倍) | 11 s | 27 s |
| 调用方和被调用方方法对,项目方法 | 20,195 | 7,615 |
| 仅此工具发现 | 12,936 | 356 |
| 方法名只声明一次的方法的调用方文件(grep 得到 2,408 个) | 2,407 个,无误报 | 902 个,无误报 |
| 关联到订阅者的事件、分发 | 26,135 | 0,0 |
| 关联到控制器的路由 | 266 | 0 |
- phpgraph 独有的调用对是根据已声明类型解析的,也包括继承方法和
instanceof:抽查的 14 个全部正确。即使方法名唯一,graphify 也会漏掉对局部变量和调用链的调用($sut->hasUser()、$this->getDateTimeFactory()->getStartOfMonth())。 - graphify 的 356 个调用对中,有 243 个将 phpgraph 关联到同名另一个方法的调用关联了过去:抽查的 12 个中,phpgraph 有 11 个正确,graphify 在第 12 个上正确(phpgraph 根据声明的返回类型
AbstractWidgetType跟踪到继承方法;该对象是一个覆盖了此方法的子类)。其余 113 个中,抽查的 12 个有 11 个错误:对另一个对象的调用被关联到当前类中同名的方法(Doctrine 的$em->rollback()被关联到TimesheetRepository::rollback());第 12 个正确,这是对一个没有返回类型的辅助对象的调用,phpgraph 将其计为未知接收方。 - phpgraph 在 2,408 个文件中唯一的漏项:
$user->isFirstDayOfWeekSunday()。loadUserByIdentifier()声明其返回 Symfony 的UserInterface,但该接口没有此方法。 - graphify 有而 phpgraph 没有的功能:支持其他语言(Kimai 的 JavaScript)、社区、交互式可视化,以及将文档纳入图中。
Sylius(语料库提交版本),2026 年 10 月,不使用任何 LLM:phpgraph 1.0.2、codebase-memory-mcp 0.11.0 和 graphify 0.9.76(--code-only),在同一台机器上,通过 Docker 运行。调用按位置逐一比较(文件、行、方法),差异样本已在代码中核查。
| phpgraph | codebase-memory-mcp | graphify | |
|---|---|---|---|
| 构建 | 10 s | 17 s | 32 s |
| 关联到项目方法的调用位置 | 22,870 | 32,274 | 4,005 |
| 其中根据类型解析 | 22,747 | 4,314 | — |
找到的 OrderItemQuantityModifierInterface::modify() 调用方(代码中有 17 个) | 17 个,无误报 | 11 个,2 个误报 | 0 个,1 个误报 |
| 关联到消息的 Messenger 处理器 | 206 | 0 | 0 |
| 容器注入、路由 | 1,360,188 | 0,1 个(误报) | 0,0 |
- codebase-memory-mcp 在其根据类型解析的调用中,有 99.7% 与 phpgraph 一致。其余边是根据方法名猜测的(置信度为 0.04 到 0.44):对于两者都关联的同一个调用,它们在 9,400 个位置上意见不一;额外的 11,300 条边大多是将对
vendor/的调用附加到同名的项目方法上(抽查的 20 个中有 19 个如此)。 - graphify 会将
$this->other->foo()关联到当前类的foo():对$this->foo()来说正确;对抽查的 10 个差异全部错误。 - codebase-memory-mcp 有而 phpgraph 没有的功能:支持其他语言、全文和向量搜索、Cypher 查询。
常见问题
phpgraph 会把我的代码发送到其他地方吗? 不会。它在本地运行,不会发起网络请求,也不使用大模型。
它会执行我的代码吗? 不会。它会解析 PHP 文件并读取 JSON、YAML 和 XML 配置。即使是 autoload_classmap.php 和 PHP 服务配置器,也只会被解析,不会被引入。
它能分析哪些 PHP 版本? 任何 nikic/php-parser 5 能解析的版本:PHP 7 和 8 的语法,以及 PHP 4 和 5 中已移除的语法($s{0}、=& new、08 这类八进制数)。对于最新语法无法解析的文件,它会尝试使用旧语法。phpgraph 本身需要 PHP 8.2 或更高版本。
它能与哪些 Agent 配合使用? 任何通过 stdio 通信的 MCP 客户端:Sherpa、Claude Code、Codex、Cursor、Claude Desktop 等。mcp-config 会输出适用于常见客户端的配置。
它能处理多大的项目? 语料库中最大的项目 Akeneo 有 8,400 个文件:完整构建约需 22 秒和 730 MB(在 Docker 中且未启用 opcache),编辑后重建约需 3 秒。
限制
- 只读取 PHP:
overview会列出混合仓库中的其他语言(它们对 PHP 部分的调用不会建立关联),MCP 指令也会在项目完全没有 PHP 时立即说明。 - 对全局函数的调用不会成为图中的边,不过函数的返回类型会决定对其返回值的调用类型(
now()->addDay()、response()->json());动态调用($this->$name()、__call)只会通过类文档中说明的方法解析(@method、@mixin)。 - 文档块由 phpstan/phpdoc-parser 读取:类型可以跨多行书写;单行文档块中的标签可以与另一个标签写在同一行;旧代码中虽不符合 PHPDoc 规范、但实际使用的写法会按其含义读取(不带返回类型的
callable(Foo $x)、?A|B、@method static NAME(): Type)。文档块中的泛型也会读取(@template、@extends Repository<Order>、@return Collection<int, Item>、ScalarNodeDefinition<$this>),包括项目和vendor/中的文档块:模板会根据接收者的类型以及将其传递下去的父类绑定,foreach会遍历任何带类型的集合中的元素。对于联合类型(Response|ResponseFactory)或交叉类型(Countable&Named),无论是文档声明还是代码声明,都会对每个声明了该方法的成员进行调用;传给方法的闭包会获得该方法文档所述的参数(@param callable($this): mixed、Laravel 的when(fn ($query) => ...));在 trait 的使用处,@use HasFactory<HotelFactory>会绑定其模板。方法的模板会根据每次调用的实参绑定:实参指定的类(带有@param class-string<T>的getRepository(Order::class))、值的类型(@param T $value)、闭包的返回值(@param callable(): T、其 return 语句或原生类型)。@phpstan-return和@psalm-return的读取优先级高于@return;如果实参无法决定条件类型(TAsync is false ? Response : PromiseInterface),则取其各分支的联合类型。由@property和@property-read文档声明的魔术属性会延续调用链;对于 Eloquent 模型,转换规则('paid_at' => 'datetime'表示 Carbon、枚举类型或转换类的TGet)、时间戳以及作为属性读取的关联($page->book、book()->getResults()的返回值)也会延续调用链。未声明返回类型的方法会返回其所重写方法声明的类型(@inheritDoc)。global $wpdb的类型取自函数的@global wpdb $wpdb;没有该标签时,类型仍为未知。不会计算类型条件($id is array ? Collection : Model),文档中返回类型为mixed的 facade 方法(DB::transaction())仍为未知。 - 局部变量会根据函数各分支的执行情况追踪:
if/elseif/else、switch、match、try/catch、三元表达式、??或循环汇合处的变量,会持有所有能到达该处的分支赋予的类型(以return、throw、exit、break或continue结束的分支不会到达该处,null和标量不会增加类型)。instanceof、is_a()、assert()、&&/||和match (true)会在条件成立处收窄变量类型;否定测试后紧接退出(if (!$x instanceof Foo) { return; })会收窄其后代码中的类型。对这样的变量进行调用时,会关联到它可能持有的每个类的方法:如果每个分支都能确定类型,则为 INFERRED;如果有一个分支的类型仍未知,则为 AMBIGUOUS。调用会根据其可调用对象在项目或vendor/中的文档收窄实参类型:包括@phpstan-assert、@psalm-assert及其-if-true/-if-false形式,也包括通过class-string实参绑定的模板(WordPress 的is_wp_error()、Webmozart\Assert\Assert::isInstanceOf($x, Order::class)、PHPUnit 的assertInstanceOf());只会查找名称类似断言的方法(is…、assert…、ensure…、check…、expect…、validate…、verify…,以及任何…Assert或…Assertion类的静态方法)。只会收窄变量,不会收窄属性($this->x instanceof Foo);循环体只读取一次(在循环末尾赋予的类型不会在循环开头被读取)。 - 库对调用的处理方式若无法从签名中得知,则由其 provider 提供(
src/Builder/Libraries):PSR-11 容器按类获取时会返回该类的实例($container->get(Mailer::class));Eloquent 模型的属性类型取自其转换规则、时间戳和关联。 - 调用链会在魔术方法处,以及返回类型不可用的依赖处停止。PHP 自带的类(
DateTime、ArrayObject、SplObjectStorage、PDO等)依据一次性生成的签名(tools/internal-classes.php)识别;intl和mysqli扩展中的类则不会识别。 - 未安装
vendor/时,调用链会在遇到第一个依赖处停止。 - 不会读取运行时构建的服务(编译器传递、bundle 扩展、根据仅在运行时才知道的值计算出的 ID)、XML 路由(但
phpgraph.yaml中映射的 action 文件除外)、在 XML 或 YAML 中声明的 API Platform 资源,以及 OpenAPI 和 protobuf 以外的 API schema(GraphQL、AsyncAPI);不会跟踪经 gateway 重写的 OpenAPI 路径;无法建立关联的注入会由overview列出。 - 假定两个服务声明的同名消息类是同一个类。
贡献
欢迎提交问题和拉取请求。请参阅 CONTRIBUTING.md:composer check 必须通过(php-cs-fixer、PHPStan 最高级别、PHPUnit),分析方面的更改则依据语料库进行评估。如果未安装 PHP,可通过 bin/dev composer check 在 Docker 中运行所有检查。
各版本间保持稳定的内容(命令、MCP 工具、graph.json 格式)列于 docs/STABILITY.md;更改记录见更新日志。
发布版本时,只需推送一个版本标签:发布工作流会构建 PHAR、适用于 Linux、macOS 和 Windows 的可执行文件(tools/build-binary.sh),以及 Docker 镜像 ghcr.io/hbenabdallah/phpgraph。
许可证
phpgraph 基于 MIT License 开源:你可以出于任何目的使用、复制、修改、合并、发布、分发、再许可和销售,但必须随附版权声明和许可证文本。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。