服务容器是依赖注入的执行引擎,负责解析依赖、实例化对象并注入依赖;所有服务默认单例,支持构造函数注入、方法注入和类型提示自动装配,配置通过services.yaml声明式定义。

它不是“一个类”或“一个配置文件”,而是一个运行时对象,负责按需创建、复用、注入和管理 PHP 对象(即服务)——你写的 App\Service\Logger、Doctrine\ORM\EntityManager、甚至 Twig\Environment,全由它统一调度。
服务容器本质是依赖注入的执行引擎
它不干别的,就做三件事:解析依赖关系、实例化对象、把依赖塞进目标对象。比如你声明 MyService 构造函数要一个 CacheInterface,容器不会让你手动 new 一个 ArrayAdapter 再传进去;它读取配置,发现 cache.app 服务实现了该接口,就自动把那个实例塞进来。
- 不写
new MyService(new ArrayAdapter(...)),避免硬编码和耦合 - 所有服务默认是单例(
shared: true),同一请求中多次获取MyService得到的是同一个实例 - 容器本身也支持被注入——比如你在自定义命令里用
ContainerInterface,拿到的就是它自己
services.yaml 里写的不是“代码”,是容器的装配说明书
YAML 中每一项都是对容器行为的声明式描述,不是执行逻辑。例如:
services:
App\Service\PaymentProcessor:
arguments:
$logger: '@monolog.logger.payment'
$httpClient: '@http_client'
calls:
- [setRetryPolicy, [3]]
这段配置意味着:
- 容器知道怎么构造
PaymentProcessor:调它的构造函数,传入两个已注册的服务 - 构造完还会立刻调用
setRetryPolicy(3)方法——这是“方法注入”,比构造函数更灵活 -
@monolog.logger.payment这种带@前缀的写法,是告诉容器“这里要注入一个服务”,不是字符串字面量 - 漏写
@(比如写成'monolog.logger.payment')会导致传入字符串而非服务实例,运行时报错Argument #1 ($logger) must be of type LoggerInterface, string given
控制器里直接类型提示参数,其实是容器在背后调用 get()
你写这个控制器方法:
public function checkout(PaymentProcessor $processor): Response
Symfony 路由器根本不会自己去 new PaymentProcessor。它把请求转给容器,容器查表发现 PaymentProcessor 是个已注册服务,且没被标记为 autowire: false,就执行 $container->get('App\Service\PaymentProcessor'),再把结果传进来。
- 如果该服务构造函数参数名是
$cache,但容器里只注册了cache.app和cache.system,它会按类型匹配(CacheInterface)再按优先级选,而不是按名字硬绑 - 若多个服务实现同一接口,必须显式用
bind:或argument:指定,否则容器会抛出Cannot autowire service...错误 - 类型提示用接口(如
MailerInterface)比用具体类更安全,因为容器能根据配置切换实现,比如从Symfony\Component\Mailer\Mailer切到测试用的NullTransport
真正容易被忽略的点是:容器编译后生成的 PHP 类(src/Kernel.php 里 buildContainer() 产出的那个)是纯函数式、无反射的——所以 dump($container) 看不到服务列表,get() 失败也不会告诉你“哪个依赖没配好”,只会报底层异常。调试得靠 bin/console debug:container 和看编译日志。











