Tempest 框架核心解析:“发现” 机制的工作原理、优势与性能优化

PHP教程 2025-08-19

Tempest 框架的核心是一个名为“发现(discovery)”的概念。这是 Tempest 区别于其他所有框架的标志性特性。虽然 Symfony、Laravel 等框架也具备有限的“发现”能力以提升便利性,但 Tempest 从“发现”机制起步,并将其打造为驱动所有功能的核心动力。本文将解释“发现”机制的工作原理、为何它如此强大,以及如何轻松构建自定义“发现”逻辑。

“发现”机制的工作原理

“发现”的理念很简单:让框架理解你的代码,这样你就无需关注配置或启动流程。当我们说 Tempest 是“不挡路的框架”时,这主要归功于“发现”机制。

我们从一个示例开始:控制器方法,写法如下:

use TempestRouterGet;use TempestViewView;final class BookController{    #[Get('/books')]
    public function index(): View
    { /* … */ }
}

你可以把这个文件放在项目的任何位置,Tempest 会自动识别它是一个控制器方法,并将路由注册到路由器中。单看这一点,可能不算特别惊艳——比如 Symfony 也有类似功能。但我们再看几个更多示例。

事件处理器通过 #[EventHandler] 注解标记,它处理的具体事件由方法参数的类型决定:

use TempestEventBusEventHandler;final class BooksEventHandlers{    #[EventHandler]
    public function onBookCreated(BookCreated $event): void
    {        // …
    }
}

控制台命令通过 #[ConsoleCommand] 注解被“发现”,控制台的命令定义会根据方法定义自动生成:

use TempestConsoleConsoleCommand;final readonly class BooksCommand{    #[ConsoleCommand]
    public function list(): void
    {        // ./tempest books:list
    }    #[ConsoleCommand]
    public function info(string $name): void
    {        // ./tempest books:info "Timeline Taxi"
    }
}

视图组件则通过文件名被“发现”:


    



    

类似的示例还有很多。那么,Tempest 的“发现”机制与 Symfony 或 Laravel 的“自动查找文件”有何不同?主要有两点:

  1. Tempest 的“发现”机制无处不在,字面意义上的“任何地方”。无需配置特定的扫描目录,Tempest 会扫描整个项目,包括 vendor 目录下的文件——这一点我们稍后再详细说明。

  2. “发现”机制具备可扩展性。如果你的项目或包需要“发现”新的内容?只需编写一个类就能实现。

这两个特性让 Tempest 的“发现”机制极具灵活性和强大性。它允许你按照自己喜欢的方式组织项目结构,无需遵循框架强制规定的结构——很多人都表示这是他们喜欢 Tempest 的原因之一。

那么,“发现”机制具体如何工作?本质上分为三个步骤:

  1. 首先,Tempest 会检查已安装的 Composer 依赖:项目的所有命名空间都会纳入“发现”范围,此外,所有依赖 Tempest 的包也会被纳入。

  2. 确定所有“发现”范围后,Tempest 会先扫描实现了 Discovery 接口的类。没错:“发现类”本身也会被“发现”。

  3. 最后,找到所有“发现类”后,Tempest 会遍历它们,并将所有待扫描的位置传递给每个“发现类”。每个“发现类”都能访问容器,并在容器中注册所需的内容。

我们以“路由发现”为例,看一个具体实现。以下是 RouteDiscovery 的完整代码,并添加了注释说明逻辑:

use TempestDiscoveryDiscovery;use TempestDiscoveryDiscoveryLocation;use TempestDiscoveryIsDiscovery;use TempestReflectionClassReflector;final class RouteDiscovery implements Discovery{    use IsDiscovery;    // 路由发现需要两个依赖,
    // 均通过自动注入(autowiring)获取
    public function __construct(
        private readonly RouteConfigurator $configurator,        private readonly RouteConfig $routeConfig,    ) {
    }    // 对每个可能被“发现”的类,都会调用 `discover` 方法
    public function discover(DiscoveryLocation $location, ClassReflector $class): void
    {        // 路由注册场景中,
        // 我们需要查找带有 `Route` 注解的方法
        foreach ($class->getPublicMethods() as $method) {            $routeAttributes = $method->getAttributes(Route::class);            foreach ($routeAttributes as $routeAttribute) {                // 每个带有 `Route` 注解的方法
                // 会先存储在内部,后续统一处理
                $this->discoveryItems->add($location, [$method, $routeAttribute]);
            }
        }
    }    // `apply` 方法用于在 `RouteConfig` 中注册路由
    // `discover` 和 `apply` 方法分离是为了缓存,
    // 本文后续会详细说明这一点
    public function apply(): void
    {        foreach ($this->discoveryItems as [$method, $routeAttribute]) {            $route = DiscoveredRoute::fromRoute($routeAttribute, $method);            $this->configurator->addRoute($route);
        }        if ($this->configurator->isDirty()) {            $this->routeConfig->apply($this->configurator->toRouteConfig());
        }
    }
}

如你所见,逻辑并不复杂。实际上,由于需要做一些路由优化,“路由发现”已经算是相对复杂的“发现”实现了。再看一个非常简单的“发现”实现示例——这个是专门为本文档网站编写的自定义“发现”类,用于查找所有实现了 Projector 接口的类:

use TempestDiscoveryDiscovery;use TempestDiscoveryDiscoveryLocation;use TempestDiscoveryIsDiscovery;use TempestReflectionClassReflector;final class ProjectionDiscovery implements Discovery{    use IsDiscovery;    public function __construct(
        private readonly StoredEventConfig $config,    ) {}    public function discover(DiscoveryLocation $location, ClassReflector $class): void
    {        if ($class->implements(Projector::class)) {            $this->discoveryItems->add($location, $class->getName());
        }
    }    public function apply(): void
    {        foreach ($this->discoveryItems as $className) {            $this->config->projectors[] = $className;
        }
    }
}

相当简单,对吧?尽管简单,但“发现”机制的能力极强,也是 Tempest 区别于其他框架的关键。

缓存与性能

“等等,这样做性能肯定不行吧?”——这是我听到 Aidan 提议“Tempest 的‘发现’机制应扫描所有项目和 vendor 文件”时的第一反应。顺便提一句,Aidan 是 Tempest 的另外两位核心贡献者之一。

Aidan 说:“别担心,肯定没问题。”事实也确实如此,不过有几个注意事项需要说明。

首先,在生产环境中,不会执行这些“代码扫描”操作。这就是为什么 discover()apply() 方法是分离的:discover() 方法负责判断“是否需要发现某个内容”并做好准备,apply() 方法则会获取准备好的数据并存储到正确的位置。换句话说:discover() 方法中执行的所有操作都会被缓存。

但本地开发环境是个例外——由于你会不断修改代码,无法对文件进行缓存。试想一下:每次添加新的控制器方法都要清除“发现”缓存,这得多麻烦?不过确实有解决方案:项目文件无法缓存,但所有 vendor 文件可以缓存——它们只会在执行 composer up 时更新。这就是所谓的“部分发现缓存(partial discovery cache)”:只缓存 vendor 目录的“发现”结果,不缓存项目文件的“发现”结果。可通过环境变量切换缓存模式:

# .envDISCOVERY_CACHE=falseDISCOVERY_CACHE=trueDISCOVERY_CACHE=partial

如果启用了“完整缓存”或“部分缓存”,还需要额外一步操作:部署后或更新 Composer 依赖后,必须重新生成“发现”缓存:

~ ./tempest discovery:generate  │ Clearing discovery cache  │ ✔ Done in 132ms.  │ Generating discovery cache using the all strategy  │ ✔ Done in 411ms.

对于本地开发,tempest/app 脚手架项目已预先配置好 Composer 钩子;如果你的项目不是基于 tempest/app 创建的,也可以手动添加:

{
  "scripts": {
    "post-package-update": [
      "@php ./tempest discovery:generate"
    ]
  }}

另外提一句:我们曾通过“生成数千个文件模拟真实项目”的方式,对“无缓存发现”的性能进行了基准测试,测试代码可在此处查看。结果显示,“发现”机制对本地开发的性能影响微乎其微。

话虽如此,我们仍有优化空间让“发现”机制更快。例如,可基于项目的 Git 状态,只对“实际修改过的文件”执行实时“发现”。这些优化可能在未来需要时实施,但在充分测试当前实现之前,我们不会进行“过早优化”。因此,如果你在使用 Tempest 时遇到与“发现”机制相关的性能问题,一定要提交 Issue——我们将非常感谢你的反馈!

以上就是对“发现”机制的深入解析。我愿意将它比作 Tempest 的“心跳”。得益于“发现”机制,我们可以省去大部分配置——因为“发现”会直接分析代码,并根据代码内容做出决策。它还允许你按任意方式组织项目结构;Tempest 不会强制要求你“控制器放这里,模型放那里”。

你可以随心所欲地开发,Tempest 会自动适配。为什么?因为它是真正“不挡路”的框架。