pHP Runtime 源码逐行解析(symfony/runtime)

  一、PHP Runtime 是干啥的?(先讲清楚问题)

  传统 PHP 的死循环

  浏览器 → Apache/Nginx → fork 一个 PHP 进程 → 跑 index.php → 输出 → 进程死掉

  每次请求都要重新加载框架、重新连接数据库、重新解析路由 —— 像每次开车都要把发动机拆了再装。

  现代 PHP Runtime 的玩法

  ┌───────────────────┬───────────────────┬───────┐
  │      Runtime      │       玩法        │ 速度  │
  ├───────────────────┼───────────────────┼───────┤
  │ 传统 PHP-FPM      │ 每次请求重启      │ 慢    │
  ├───────────────────┼───────────────────┼───────┤
  │ Swoole            │ 协程 + 常驻内存   │ 5-10x │
  ├───────────────────┼───────────────────┼───────┤
  │ RoadRunner        │ Go 写的 worker 池 │ 5-10x │
  ├───────────────────┼───────────────────┼───────┤
  │ FrankenPHP        │ Caddy 内嵌 PHP    │ 4-7x  │
  ├───────────────────┼───────────────────┼───────┤
  │ Bref / AWS Lambda │ 无服务器          │ 按需  │
  ├───────────────────┼───────────────────┼───────┤
  │ ReactPHP          │ 事件循环          │ 异步  │
  └───────────────────┴───────────────────┴───────┘

  痛点:同一份业务代码,在 Swoole 下入口要写一种,在 RoadRunner 下入口要写另一种,在 Lambda 下又要写第三种 —— 每换一个
  runtime,index.php 全得重写。

  symfony/runtime 出场

  它做了一件事:让你的 index.php 永远只写一份,具体跑在哪个 runtime 上,改一个环境变量就行:

  APP_RUNTIME=Runtime\Swoole\Runtime
  APP_RUNTIME=Runtime\FrankenPhpSymfony\Runtime
  APP_RUNTIME=Runtime\RoadRunnerSymfony\Runtime
  APP_RUNTIME=Bref\SymfonyRuntime\Runtime

  代码完全不用动。这就是 PHP 生态里最优雅的一个组件之一。

  ---
  二、核心架构(三个接口讲清整套设计)

  三个接口的金三角

     ┌─────────────────────┐
     │  RuntimeInterface   │  ← 入口工厂
     └──────────┬──────────┘
                │ 生产
        ┌───────┴────────┐
        │                │
        ▼                ▼
  ┌─────────────┐  ┌──────────────┐
  │  Resolver   │  │   Runner     │
  │  解析闭包    │  │  执行应用     │
  └─────────────┘  └──────────────┘

  - Runtime:工厂,负责造出下面两个
  - Resolver:把你 return 的那个 function (array $context) {...} 解析成 [$application, $args]
  - Runner:拿到 $application(可能是 Symfony Kernel、HttpFoundation Response、Console Application、PSR-15
  Handler...),知道怎么跑它

  这是经典的策略模式 + 工厂方法。下面逐行看源码。

  ---
  三、源码逐行解析(基于 Symfony Runtime 6.4 真实源码)

  3.1 RuntimeInterface — 总司令

  <?php
  namespace Symfony\Component\Runtime;

  interface RuntimeInterface
  {
      public function getResolver(callable $callable, ?\ReflectionFunction $reflector = null): ResolverInterface;

      public function getRunner(?object $application): RunnerInterface;
  }

  逐行大白话:

  - getResolver(callable $callable, ?\ReflectionFunction $reflector):
    - 输入是用户写的那个 function (array $context) {...} 闭包
    - 第二个参数是反射对象,Runtime 用它来看闭包的参数列表和返回类型,从而决定该注入啥
    - 输出是一个解析器,可以"展开"这个闭包
  - getRunner(?object $application):
    - 输入是闭包执行后返回的对象(Kernel / Response / Application…)
    - 输出是一个能正确运行这个对象的 runner
    - ?object 允许 null:有些 runtime 直接执行函数无返回值

  为啥分两步? 因为闭包里面要的参数(比如 $request$kernel)和外面返回的东西(Response)是两码事,分开处理更清晰。

  ---
  3.2 ResolverInterface — 闭包解析器

  <?php
  namespace Symfony\Component\Runtime;

  interface ResolverInterface
  {
      /**
       * @return array{0: callable, 1: array}  返回 [callable, args]
       */
      public function resolve(): array;
  }

  大白话:用户写的入口是

  return function (Request $request, ContainerInterface $container) {
      return new Response('hello');
  };

  Runtime 看到这个闭包,通过 ReflectionFunction 拿到形参类型 Request 和
  ContainerInterface,然后实例化(或从容器拿)这俩对象,最后 resolve() 返回:

  [
      $closure,                        // 那个闭包本身
      [$requestInstance, $container],  // 已经准备好的参数
  ]

  外层只要 $callable(...$args) 就能调用,实现了依赖注入到闭包的形参。

  ---
  3.3 RunnerInterface — 应用执行器

  <?php
  namespace Symfony\Component\Runtime;

  interface RunnerInterface
  {
      public function run(): int;
  }

  大白话:返回退出码(0 = 成功,非 0 = 失败),直接被 PHP 的 exit() 用。这是 Unix 风格 —— 跟所有 CLI 工具一致。

  ---
  3.4 GenericRuntime — 通用 Runtime(核心实现,逐行剖析)

  这是整个组件最关键的类,我把真实源码精简后逐行讲解:

  <?php
  namespace Symfony\Component\Runtime;

  use Symfony\Component\Runtime\Internal\BasicErrorHandler;
  use Symfony\Component\Runtime\Resolver\ClosureResolver;
  use Symfony\Component\Runtime\Resolver\DebugClosureResolver;
  use Symfony\Component\Runtime\Runner\ClosureRunner;

  class GenericRuntime implements RuntimeInterface
  {
      /** @var array<string, mixed> */
      protected array $options;

      /**
       * @param array{
       *   debug?: bool,
       *   env?: string,
       *   disable_dotenv?: bool,
       *   project_dir?: string,
       *   prod_envs?: string[],
       *   test_envs?: string[],
       *   use_putenv?: bool,
       *   runtimes?: array,
       *   error_handler?: string|false,
       *   env_var_name?: string,
       *   debug_var_name?: string
       * } $options
       */
      public function __construct(array $options = [])
      {
          $options['env_var_name'] ??= 'APP_ENV';
          $debugKey = $options['debug_var_name'] ??= 'APP_DEBUG';

          // 1. 推导 debug 模式
          $debug = $options['debug'] ?? $_SERVER[$debugKey] ?? $_ENV[$debugKey] ?? true;
          if (!\is_bool($debug)) {
              $debug = filter_var($debug, FILTER_VALIDATE_BOOL);
          }

          // 2. 把 debug 同步到 env / putenv,让其他代码也能读到
          if ($debug) {
              umask(0000);
              $_SERVER[$debugKey] = $_ENV[$debugKey] = '1';

              if (false !== ($options['error_handler'] ?? BasicErrorHandler::class)) {
                  $errorHandler = new ($options['error_handler'] ?? BasicErrorHandler::class)(true);
                  set_error_handler($errorHandler);
              }
          } else {
              $_SERVER[$debugKey] = $_ENV[$debugKey] = '0';
          }

          $this->options = $options;
      }

      public function getResolver(callable $callable, ?\ReflectionFunction $reflector = null): ResolverInterface
      {
          // 1. 把 callable 变成 Closure(统一类型)
          if (!$callable instanceof \Closure) {
              $callable = \Closure::fromCallable($callable);
          }

          $reflector ??= new \ReflectionFunction($callable);
          $parameters = $reflector->getParameters();
          $arguments = function () use ($parameters) {
              $args = [];
              foreach ($parameters as $parameter) {
                  $type = $parameter->getType();
                  $args[] = $this->getArgument($parameter, $type instanceof \ReflectionNamedType ? $type->getName() :
  null);
              }
              return $args;
          };

          // 2. debug 模式用 DebugClosureResolver(报错更详细)
          if ($_SERVER[$this->options['debug_var_name']]) {
              return new DebugClosureResolver($callable, $arguments);
          }

          return new ClosureResolver($callable, $arguments);
      }

      public function getRunner(?object $application): RunnerInterface
      {
          if (null === $application) {
              $application = static function () {
                  // 啥也不干,正常退出
              };
          }

          // 应用本身是 RunnerInterface,直接用
          if ($application instanceof RunnerInterface) {
              return $application;
          }

          // 应用是 callable,包装成 ClosureRunner
          if (\is_callable($application)) {
              return new ClosureRunner($application);
          }

          throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', static::class,
  get_debug_type($application)));
      }

      /**
       * 关键方法:根据形参类型,造一个参数出来
       */
      protected function getArgument(\ReflectionParameter $parameter, ?string $type): mixed
      {
          if ('array' === $type) {
              // 形参是 array $context,就把 $_SERVER + $_ENV 合并塞进去
              switch ($parameter->name) {
                  case 'context':
                      $context = $_SERVER;
                      if ($_ENV && !isset($_SERVER['PATH']) && !isset($_SERVER['Path'])) {
                          $context += $_ENV;
                      }
                      return $context;

                  case 'argv':
                      return $_SERVER['argv'] ?? [];

                  case 'request':
                      return ['query' => $_GET, 'body' => $_POST, 'files' => $_FILES, 'session' => $_SESSION ?? null];
              }
          }

          if (Closure::class === $type) {
              // 形参是 Closure,啥也不传(后续 Symfony Runtime 会处理)
          }

          if ($parameter->isDefaultValueAvailable()) {
              return $parameter->getDefaultValue();
          }
          if ($parameter->allowsNull()) {
              return null;
          }

          throw new \InvalidArgumentException(sprintf('Cannot resolve argument "$%s" of type "%s".', $parameter->name,
  $type ?? 'mixed'));
      }
  }

  逐行大白话:

  - 构造函数:吃配置选项,推导 APP_DEBUG,调试模式下注册 BasicErrorHandler(把 PHP 错误变漂亮异常)。umask(0000)
  是让生成的文件权限默认 666 而不是 644,方便容器场景。
  - getResolver():
    - 把任何 callable 都变成 Closure,统一处理
    - 用反射拿到所有形参
    - 准备一个惰性闭包 $arguments —— 注意这里没有立刻调用 getArgument(),而是返回一个闭包,后续 resolver
  真正需要时才调用。这是为了让框架的初始化顺序可控
    - 调试模式返回带异常上下文的 DebugClosureResolver,生产返回更轻量的 ClosureResolver
  - getRunner():三种应用类型分别处理:
    - 应用是 null:啥也不干
    - 应用实现了 RunnerInterface:它自己就是 runner
    - 应用是 callable:包成 ClosureRunner
    - 都不是:抛异常 —— 这里就是子类(如 SymfonyRuntime)要拓展的地方
  - getArgument():这是依赖注入的核心。看到形参 array $context 就给 $_SERVER + $_ENV,看到 array $argv 就给命令行参数 ——
  约定优于配置。

  ---
  3.5 SymfonyRuntime — 让 Symfony 一切对象都跑起来

  这个类继承 GenericRuntime,只做一件事:认识更多 Symfony 类型。

  <?php
  namespace Symfony\Component\Runtime;

  use Symfony\Bundle\FrameworkBundle\Console\Application as ConsoleApplication;
  use Symfony\Component\Console\Application as BareConsoleApplication;
  use Symfony\Component\Console\Command\Command;
  use Symfony\Component\Console\Input\InputInterface;
  use Symfony\Component\Console\Output\OutputInterface;
  use Symfony\Component\HttpFoundation\Request;
  use Symfony\Component\HttpFoundation\Response;
  use Symfony\Component\HttpKernel\HttpKernelInterface;
  use Symfony\Component\HttpKernel\TerminableInterface;
  use Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner;
  use Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner;
  use Symfony\Component\Runtime\Runner\Symfony\ResponseRunner;

  class SymfonyRuntime extends GenericRuntime
  {
      public function getRunner(?object $application): RunnerInterface
      {
          // HttpKernel(包括 Symfony Kernel)
          if ($application instanceof HttpKernelInterface) {
              return new HttpKernelRunner(
                  $application,
                  Request::createFromGlobals(),
                  $this->options['debug'] ?? false
              );
          }

          // 直接返回 Response(微框架风格)
          if ($application instanceof Response) {
              return new ResponseRunner($application);
          }

          // 控制台应用
          if ($application instanceof BareConsoleApplication) {
              return new ConsoleApplicationRunner($application, $this->options['default_command'] ?? 'list');
          }

          // 单个 Command(简化用法)
          if ($application instanceof Command) {
              $console = new BareConsoleApplication();
              $console->add($application);
              $console->setDefaultCommand($application->getName(), true);
              return new ConsoleApplicationRunner($console, $application->getName());
          }

          // 其他类型,交给父类
          return parent::getRunner($application);
      }

      protected function getArgument(\ReflectionParameter $parameter, ?string $type): mixed
      {
          return match ($type) {
              Request::class => Request::createFromGlobals(),
              InputInterface::class => new \Symfony\Component\Console\Input\ArgvInput(),
              OutputInterface::class => new \Symfony\Component\Console\Output\ConsoleOutput(),
              BareConsoleApplication::class, ConsoleApplication::class => new ConsoleApplication(),
              default => parent::getArgument($parameter, $type),
          };
      }
  }

  大白话:

  - getRunner() 多识别了 4 种类型:HttpKernel / Response / Console Application / 单个 Command
  - getArgument() 多识别了 4 种类型:形参类型如果是 Request、InputInterface、OutputInterface、Application,自动给一个

  这就是为什么 Symfony 项目的 index.php 可以写得跟微框架一样简洁:

  require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

  return function (array $context) {
      return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
  };

  ---
  3.6 ClosureResolver — 最简单的解析器

  <?php
  namespace Symfony\Component\Runtime\Resolver;

  use Symfony\Component\Runtime\ResolverInterface;

  class ClosureResolver implements ResolverInterface
  {
      private \Closure $closure;
      private \Closure $arguments;

      public function __construct(\Closure $closure, \Closure $arguments)
      {
          $this->closure = $closure;
          $this->arguments = $arguments;
      }

      public function resolve(): array
      {
          // 这里才真正调用 $arguments() 闭包,触发参数解析
          return [$this->closure, ($this->arguments)()];
      }
  }

  大白话:它就是个数据容器,直到调用 resolve() 才真正去算参数。这种延迟计算让 runtime 有机会在 resolve 之前注册更多东西。

  ---
  3.7 HttpKernelRunner — 跑 Symfony HttpKernel 的 runner

  <?php
  namespace Symfony\Component\Runtime\Runner\Symfony;

  use Symfony\Component\HttpFoundation\Request;
  use Symfony\Component\HttpFoundation\Response;
  use Symfony\Component\HttpKernel\HttpKernelInterface;
  use Symfony\Component\HttpKernel\TerminableInterface;
  use Symfony\Component\Runtime\RunnerInterface;

  class HttpKernelRunner implements RunnerInterface
  {
      public function __construct(
          private HttpKernelInterface $kernel,
          private Request $request,
          private bool $debug = false,
      ) {}

      public function run(): int
      {
          // 1. 处理请求拿到响应
          $response = $this->kernel->handle($this->request);

          // 2. 把响应发出去(写 header + 写 body)
          $response->send();

          // 3. 如果 kernel 实现了 TerminableInterface(默认 Symfony Kernel 实现了)
          //    调用 terminate(),这里会跑所有"响应已发送但还有事要做"的 listener
          //    比如:落日志、发送邮件、清理 session
          if ($this->kernel instanceof TerminableInterface) {
              $this->kernel->terminate($this->request, $response);
          }

          return 0;
      }
  }

  大白话:这是 Symfony 请求生命周期的标准三部曲:handle → send → terminate。其中 terminate() 是个隐藏知识点 ——
  它在响应发给浏览器之后才执行,适合干"用户不需要等的"事情(比如发邮件)。这就是为啥用 Symfony 写的网站显得快。

  ---
  3.8 ConsoleApplicationRunner — 跑命令行应用

  <?php
  namespace Symfony\Component\Runtime\Runner\Symfony;

  use Symfony\Component\Console\Application;
  use Symfony\Component\Runtime\RunnerInterface;

  class ConsoleApplicationRunner implements RunnerInterface
  {
      public function __construct(
          private Application $application,
          private ?string $defaultCommand = null,
      ) {}

      public function run(): int
      {
          if (null !== $this->defaultCommand) {
              $this->application->setDefaultCommand($this->defaultCommand);
          }
          return $this->application->run();
      }
  }

  大白话:把 Application::run() 的退出码透传出来。短小精悍。

  ---
  四、autoload_runtime.php 的魔法(整套机制的灵魂)

  4.1 这文件是怎么来的?

  关键:这个文件不是用户写的,也不在 git 里。它是 composer install 时,由 symfony/runtime 自带的 Composer 插件动态生成的。

  源码在 Internal/ComposerPlugin.php(精简版):

  <?php
  namespace Symfony\Component\Runtime\Internal;

  use Composer\Composer;
  use Composer\IO\IOInterface;
  use Composer\Plugin\PluginInterface;
  use Composer\EventDispatcher\EventSubscriberInterface;
  use Composer\Script\ScriptEvents;

  class ComposerPlugin implements PluginInterface, EventSubscriberInterface
  {
      public function activate(Composer $composer, IOInterface $io): void {}
      public function deactivate(Composer $composer, IOInterface $io): void {}
      public function uninstall(Composer $composer, IOInterface $io): void {}

      public static function getSubscribedEvents(): array
      {
          return [
              ScriptEvents::POST_AUTOLOAD_DUMP => 'onPostAutoloadDump',
          ];
      }

      public function onPostAutoloadDump($event): void
      {
          $composer = $event->getComposer();
          $extra = $composer->getPackage()->getExtra();

          // 默认用 SymfonyRuntime
          $runtimeClass = $extra['runtime']['class'] ?? 'Symfony\Component\Runtime\SymfonyRuntime';
          $autoloadDir = $composer->getConfig()->get('vendor-dir');

          $template = file_get_contents(__DIR__ . '/../autoload_runtime.template');
          $code = strtr($template, [
              '{$runtimeClass}' => var_export($runtimeClass, true),
              '{$runtimeOptions}' => var_export($extra['runtime'] ?? [], true),
          ]);

          file_put_contents($autoloadDir . '/autoload_runtime.php', $code);
      }
  }

  大白话:Composer 跑完 dump-autoload 之后,这个插件就被触发,读取 composer.json 的 extra.runtime 配置,然后渲染一个模板生成
   vendor/autoload_runtime.php。

  这就是为什么 你换 runtime 时只需要改 composer.json:

  {
      "extra": {
          "runtime": {
              "class": "Runtime\\Swoole\\Runtime"
          }
      }
  }

  或者环境变量:

  APP_RUNTIME=Bref\\SymfonyRuntime\\Runtime

  ---
  4.2 生成出来的 autoload_runtime.php 长啥样?

  模板渲染后大致是这样:

  <?php
  // 自动生成 - 不要手动改

  // 1. 加载 Composer autoloader
  $loader = require __DIR__ . '/autoload.php';

  // 2. 决定 Runtime 类(优先级:env > composer.json > 默认)
  $runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\Component\Runtime\SymfonyRuntime';
  $runtimeOptions = ['project_dir' => dirname(__DIR__)] + (array) ($GLOBALS['_composer_runtime_options'] ?? []);

  // 3. 拿到调用者:也就是用户写的 index.php 里的 return function(){}
  $app = require $_SERVER['SCRIPT_FILENAME'] ?? $_SERVER['PHP_SELF'] ?? array_shift($_SERVER['argv']);

  // 4. 如果 index.php 不是 return 一个 callable,而是直接写代码,那就直接退出
  if (!\is_object($app) || !\is_callable($app)) {
      exit(\is_int($app) ? $app : 0);
  }

  // 5. 实例化 Runtime
  $runtime = new $runtime($runtimeOptions);

  // 6. 解析闭包:拿到 [callable, args]
  [$app, $args] = $runtime
      ->getResolver($app)
      ->resolve();

  // 7. 调用闭包,拿到返回的 application 对象(Kernel / Response / ...)
  $app = $app(...$args);

  // 8. 包装成 runner,运行,拿退出码
  exit(
      $runtime
          ->getRunner($app)
          ->run()
  );

  这八步是整个机制的精华。我特别强调:

  1. 第 3 步的 require $_SERVER['SCRIPT_FILENAME'] 是反向 require —— 通常是 index.php require
  autoload_runtime.php,但这里反过来,autoload_runtime.php 又 require 回了 index.php,拿到 return 的那个闭包
  2. 第 4 步是兼容:你的 index.php 也可以不返回 closure,直接干完活退出 —— 老风格也兼容
  3. 第 5-8 步严格对应 Resolver / Runner 接口

  ---
  五、和各家 Runtime 集成

  5.1 Bref(AWS Lambda)

  composer require runtime/bref

  # .env
  APP_RUNTIME=Runtime\\Bref\\Runtime

  bref/symfony-runtime 的 Runtime 类(精简):

  class Runtime extends SymfonyRuntime
  {
      public function getRunner(?object $application): RunnerInterface
      {
          if ($application instanceof HttpKernelInterface) {
              // 不像 HttpKernelRunner 那样从 globals 拿 Request,
              // 而是从 Lambda 的事件里构造
              return new BrefHttpKernelRunner($application);
          }
          if ($application instanceof Handler) {
              // Lambda 原生 handler
              return new BrefHandlerRunner($application);
          }
          return parent::getRunner($application);
      }
  }

  大白话:Bref 运行时把 AWS Lambda 的事件转成 Symfony Request,跑完 Kernel 再把 Response 转回 Lambda
  响应格式。业务代码完全不用改。

  5.2 RoadRunner

  composer require runtime/roadrunner-symfony-nyholm

  APP_RUNTIME=Runtime\\RoadRunnerSymfonyNyholm\\Runtime

  它的 Runner 内部跑了一个死循环:

  class RoadRunnerRunner implements RunnerInterface
  {
      public function run(): int
      {
          // 创建 PSR-7 worker
          $worker = new PSR7Worker(Worker::create(), $factory, $factory, $factory);

          while (true) {
              try {
                  $request = $worker->waitRequest();
                  if (!$request) break;  // 收到关闭信号

                  $symfonyRequest = $this->httpFoundationFactory->createRequest($request);
                  $response = $this->kernel->handle($symfonyRequest);
                  $psr7Response = $this->psrHttpFactory->createResponse($response);

                  $worker->respond($psr7Response);

                  if ($this->kernel instanceof TerminableInterface) {
                      $this->kernel->terminate($symfonyRequest, $response);
                  }
              } catch (\Throwable $e) {
                  $worker->getWorker()->error((string) $e);
              }
          }
          return 0;
      }
  }

  大白话:这是常驻内存的关键 —— while(true) 不停从 RoadRunner 接请求,处理完发回去。Kernel 只 boot
  一次,后面所有请求复用。这就是为啥比 PHP-FPM 快几倍。

  5.3 FrankenPHP(Caddy 内嵌 PHP,2024 年最热)

  composer require runtime/frankenphp-symfony

  APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime

  Runner 用了 FrankenPHP 的 worker 模式 + frankenphp_handle_request():

  public function run(): int
  {
      $handler = static function () {
          $request = Request::createFromGlobals();
          $response = $this->kernel->handle($request);
          $response->send();
          if ($this->kernel instanceof TerminableInterface) {
              $this->kernel->terminate($request, $response);
          }
      };

      while (\frankenphp_handle_request($handler)) {
          gc_collect_cycles();  // 主动 GC,防内存泄漏
      }
      return 0;
  }

  ---
  六、自己写一个 Runtime —— 完整代码

  假设你想做一个 ReactPHP runtime 让 Symfony 跑在异步事件循环里:

  <?php
  namespace Yourorg\ReactRuntime;

  use React\EventLoop\Loop;
  use React\Http\HttpServer;
  use React\Http\Message\Response as ReactResponse;
  use React\Socket\SocketServer;
  use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
  use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
  use Symfony\Component\HttpKernel\HttpKernelInterface;
  use Symfony\Component\HttpKernel\TerminableInterface;
  use Symfony\Component\Runtime\RunnerInterface;
  use Symfony\Component\Runtime\SymfonyRuntime;

  final class Runtime extends SymfonyRuntime
  {
      public function getRunner(?object $application): RunnerInterface
      {
          if ($application instanceof HttpKernelInterface) {
              return new ReactHttpRunner(
                  $application,
                  $this->options['react_listen'] ?? '0.0.0.0:8080'
              );
          }
          return parent::getRunner($application);
      }
  }

  final class ReactHttpRunner implements RunnerInterface
  {
      public function __construct(
          private HttpKernelInterface $kernel,
          private string $listen,
      ) {}

      public function run(): int
      {
          $httpFactory = new HttpFoundationFactory();
          $psrFactory = new PsrHttpFactory(/* PSR-17 工厂们 */);

          $server = new HttpServer(function ($psrRequest) use ($httpFactory, $psrFactory) {
              $sfRequest = $httpFactory->createRequest($psrRequest);
              $sfResponse = $this->kernel->handle($sfRequest);

              if ($this->kernel instanceof TerminableInterface) {
                  Loop::futureTick(fn() => $this->kernel->terminate($sfRequest, $sfResponse));
              }

              return $psrFactory->createResponse($sfResponse);
          });

          $socket = new SocketServer($this->listen);
          $server->listen($socket);

          echo "ReactPHP runtime listening on {$this->listen}\n";
          Loop::run();
          return 0;
      }
  }

  用户用法,index.php 一字不改,只改 composer.json:

  {
      "extra": {
          "runtime": {
              "class": "Yourorg\\ReactRuntime\\Runtime",
              "react_listen": "0.0.0.0:9000"
          }
      }
  }

  这就是 symfony/runtime 设计的最终目的:让所有 PHP runtime 提供商都按这个接口对接,业务代码完全解耦。

  ---
  七、最佳实践 / 踩坑指南

  ┌────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────┐
  │                       坑                       │                              解决                              │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ 常驻内存下 Kernel 不重                         │ 用 RoadRunner Worker 的 stateless 模式,或每 N 次请求 graceful  │
  │ boot,容器里旧实例污染请求                      │ 重启                                                           │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ 单例服务持有了 Request,下次请求拿到旧的        │ 标 @kernel.reset tag,Symfony 会自动重置                        │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ 静态属性、static::$foo 跨请求残留              │ 别用静态属性存请求级数据                                       │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ 数据库连接超时                                 │ Doctrine 配 ping 或 reset_on_close: true                       │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ 内存泄漏                                       │ 主动 gc_collect_cycles(),limit 跑 N 个请求后 worker 退出       │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ 文件描述符泄漏                                 │ 资源类对象在 kernel.terminate 后释放                           │
  ├────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
  │ $_SERVER / $_GET 是上一次请求的                │ 永远从注入的 $request 拿,不从超全局变量拿                      │
  └────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────┘

  ---
  八、为啥这套设计是"最佳方式"?

  1. 接口隔离原则(ISP):三个接口各管一摊,Runtime 不关心 Runner 怎么跑,Runner 不关心 Resolver 怎么解
  2. 开闭原则(OCP):扩展新 runtime 只继承 + 重写 getRunner(),不改老代码
  3. 依赖倒置(DIP):autoload_runtime.php 依赖接口,不依赖具体实现
  4. 延迟计算:$arguments 是闭包,真正用时才执行,顺序可控
  5. 元编程优雅:Composer 插件 + 自动生成 + 反射注入,用户感知为零
  6. 零配置默认值:用反射看形参类型自动注入,不用写 services.yaml 配置入口
  7. 环境变量优先级清晰:env > composer extra > 默认,Twelve-Factor App 哲学

  ---
  总结一句话

  symfony/runtime 不是个跑代码的库,而是个让所有跑代码的库都能插进来的标准接口 —— 它做的是"PHP 运行时的 USB
  接口"。读懂这套源码,你会对策略模式 + 工厂方法 + 反射注入 + 元编程有一次质变级别的理解。建议把
  vendor/symfony/runtime/src/ 整个目录用 IDE 打开,跟着上面的导航一遍走完,不超过 2 小时,绝对值得。

更多推荐