什么是依赖注入:从硬编码到松耦合的演进
在 PHP 项目开发中,我们经常遇到这样的代码:一个类在内部直接创建它所依赖的对象。这种做法看似简单,却埋下了严重的架构隐患——类与类之间紧密耦合,无法独立测试、无法灵活替换、无法应对需求变化。依赖注入(Dependency Injection,简称 DI)正是解决这一问题的经典方案。
依赖注入的核心思想非常简单:不要在类内部创建依赖,而是从外部传入依赖。这一原则看似微小,却能从根本上改变代码的组织方式。让我们通过一个具体的例子来感受这种变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 // 硬编码依赖——耦合的写法
class UserController
{
private PDO $db;
public function __construct()
{
$this->db = new PDO('mysql:host=localhost;dbname=app', 'root', '');
}
public function profile(int $id): array
{
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
}
上面的
1 | UserController |
直接在构造函数中创建
1 | PDO |
实例。这意味着如果你想换一个数据库连接配置、想在测试中使用内存数据库、或者想用连接池替换直连——你必须修改
1 | UserController |
的源码。这违反了开闭原则(对扩展开放,对修改关闭)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 // 依赖注入——松耦合的写法
class UserController
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function profile(int $id): array
{
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
}
改动只有一个:将
1 | new PDO() |
从构造函数内部移到了参数中。但这个改动带来的收益是巨大的——
1 | UserController |
不再关心数据库连接如何创建,它只需要知道有一个
1 | PDO |
实例可用。这就是依赖注入的精髓。
依赖注入的三种形式详解
依赖注入有三种经典形式,每种都有其适用场景。理解它们的区别,是在实际项目中正确运用 DI 的基础。
构造函数注入
构造函数注入是最常见、最推荐的方式。依赖通过构造函数参数传入,在对象创建时就完成了所有必要的依赖绑定。这种方式确保对象在创建后始终处于完整、可用的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 class OrderService
{
public function __construct(
private readonly OrderRepository $orders,
private readonly PaymentGateway $payment,
private readonly NotificationService $notifier,
) {}
public function placeOrder(Order $order): OrderResult
{
$this->orders->save($order);
$result = $this->payment->charge($order->getTotal());
if ($result->isSuccess()) {
$this->notifier->sendConfirmation($order);
}
return $result;
}
}
构造函数注入的优势在于强制依赖不可遗漏——如果缺少必要的依赖,对象根本无法创建。这也使得依赖关系一目了然:只需查看构造函数签名,就能知道这个类需要什么。
Setter 注入
Setter 注入通过专门的 setter 方法传入依赖,适用于可选依赖或需要运行时动态替换的场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 class Logger
{
private ?LogHandler $handler = null;
public function setHandler(LogHandler $handler): self
{
$this->handler = $handler;
return $this;
}
public function log(string $message, string $level = 'info'): void
{
$this->handler?->write($message, $level);
}
}
Setter 注入的灵活性是一把双刃剑。对象可能在依赖未设置的情况下被调用,导致运行时错误。因此,建议只对真正可选的依赖使用 Setter 注入,核心依赖仍应通过构造函数注入。
接口注入
接口注入通过定义一个注入接口来强制类接受特定依赖,在 PHP 中较少使用,但在框架设计中很有价值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 interface CacheAwareInterface
{
public function setCache(CacheInterface $cache): void;
}
class ReportGenerator implements CacheAwareInterface
{
private ?CacheInterface $cache = null;
public function setCache(CacheInterface $cache): void
{
$this->cache = $cache;
}
public function generate(ReportRequest $request): Report
{
$cacheKey = 'report:' . md5(serialize($request));
if ($this->cache && $cached = $this->cache->get($cacheKey)) {
return $cached;
}
$report = $this->buildReport($request);
$this->cache?->set($cacheKey, $report, 3600);
return $report;
}
}
接口注入的优势在于语义明确——实现了
1 | CacheAwareInterface |
的类清楚地表明它支持缓存功能。容器可以据此自动调用注入方法。
依赖注入容器:自动装配的核心引擎
当项目中的类越来越多、依赖关系越来越复杂时,手动创建和注入依赖会变得非常繁琐。这就是依赖注入容器(DI Container)登场的时候。容器负责管理对象的创建和依赖关系的解析,让你从繁琐的手动装配中解放出来。
一个 DI 容器的核心职责有三个:
- 服务注册:将抽象(接口或类名)与具体实现绑定
- 依赖解析:自动分析构造函数参数,递归解析所有依赖
- 生命周期管理:控制对象的创建策略(每次新建、共享单例等)
让我们从零实现一个轻量级的 DI 容器,深入理解其工作原理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 class Container
{
private array $bindings = []; // 抽象 -> 具体实现的绑定
private array $instances = []; // 已创建的单例实例
private array $singletons = []; // 标记为单例的抽象
/**
* 绑定抽象到具体实现
*/
public function bind(string $abstract, string|callable $concrete): void
{
$this->bindings[$abstract] = $concrete;
unset($this->instances[$abstract], $this->singletons[$abstract]);
}
/**
* 绑定为单例
*/
public function singleton(string $abstract, string|callable $concrete): void
{
$this->bind($abstract, $concrete);
$this->singletons[$abstract] = true;
}
/**
* 解析并创建实例
*/
public function make(string $abstract): object
{
// 如果是单例且已创建,直接返回
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// 获取具体实现
$concrete = $this->bindings[$abstract] ?? $abstract;
// 如果绑定是回调函数,直接执行
if (is_callable($concrete)) {
$instance = $concrete($this);
} else {
$instance = $this->build($concrete);
}
// 如果是单例,缓存实例
if (isset($this->singletons[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
/**
* 通过反射自动构建实例
*/
private function build(string $class): object
{
$reflector = new ReflectionClass($class);
if (!$reflector->isInstantiable()) {
throw new RuntimeException("Class {$class} is not instantiable");
}
$constructor = $reflector->getConstructor();
if ($constructor === null) {
return new $class();
}
$dependencies = $this->resolveDependencies(
$constructor->getParameters()
);
return $reflector->newInstanceArgs($dependencies);
}
/**
* 递归解析构造函数参数的依赖
*/
private function resolveDependencies(array $parameters): array
{
$deps = [];
foreach ($parameters as $param) {
$type = $param->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
// 类型提示是类/接口,递归解析
$deps[] = $this->make($type->getName());
} elseif ($param->isDefaultValueAvailable()) {
// 有默认值,使用默认值
$deps[] = $param->getDefaultValue();
} else {
throw new RuntimeException(
"Cannot resolve parameter \${$param->getName()} in {$param->getDeclaringClass()->getName()}"
);
}
}
return $deps;
}
}
这个容器虽然只有不到 80 行代码,却实现了 DI 容器的核心功能。关键在于
1 | resolveDependencies |
方法——它通过反射读取构造函数参数的类型提示,然后递归地解析每个依赖。这就是”自动装配”(Autowiring)的基本原理。
接口绑定与上下文依赖
在实际项目中,我们更多地面向接口编程,而不是依赖具体实现。这就需要容器能够将接口绑定到具体实现类,并在解析时自动替换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 // 定义接口
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 0): bool;
public function delete(string $key): bool;
}
// 开发环境使用文件缓存
class FileCache implements CacheInterface { /* ... */ }
// 生产环境使用 Redis 缓存
class RedisCache implements CacheInterface { /* ... */ }
// 测试环境使用内存缓存
class ArrayCache implements CacheInterface { /* ... */ }
// 在容器中绑定
$container = new Container();
if ($env === 'production') {
$container->singleton(CacheInterface::class, RedisCache::class);
} elseif ($env === 'testing') {
$container->singleton(CacheInterface::class, ArrayCache::class);
} else {
$container->singleton(CacheInterface::class, FileCache::class);
}
// 解析时自动获得正确的实现
$cache = $container->make(CacheInterface::class);
// 生产环境:$cache 是 RedisCache 实例
// 测试环境:$cache 是 ArrayCache 实例
更复杂的场景是上下文绑定——同一个接口在不同类中需要不同的实现。例如,日志服务在支付模块中需要写入文件,在用户模块中需要发送到远程服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90 class Container
{
// ... 前面的代码省略 ...
private array $contextualBindings = [];
/**
* 为特定类定义上下文绑定
*/
public function when(string $class): ContextualBindingBuilder
{
return new ContextualBindingBuilder($this, $class);
}
public function addContextualBinding(
string $concrete,
string $abstract,
string|callable $implementation
): void {
$this->contextualBindings[$concrete][$abstract] = $implementation;
}
/**
* 修改 resolveDependencies,优先检查上下文绑定
*/
private function resolveDependenciesFor(
string $class,
array $parameters
): array {
$deps = [];
foreach ($parameters as $param) {
$type = $param->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();
// 优先检查上下文绑定
if (isset($this->contextualBindings[$class][$typeName])) {
$implementation = $this->contextualBindings[$class][$typeName];
$deps[] = is_callable($implementation)
? $implementation($this)
: $this->build($implementation);
} else {
$deps[] = $this->make($typeName);
}
} elseif ($param->isDefaultValueAvailable()) {
$deps[] = $param->getDefaultValue();
} else {
throw new RuntimeException(
"Cannot resolve \${$param->getName()} in {$class}"
);
}
}
return $deps;
}
}
class ContextualBindingBuilder
{
public function __construct(
private Container $container,
private string $class
) {}
public function needs(string $abstract): self
{
$this->abstract = $abstract;
return $this;
}
public function give(string|callable $implementation): void
{
$this->container->addContextualBinding(
$this->class,
$this->abstract,
$implementation
);
}
}
// 使用上下文绑定
$container->when(PaymentService::class)
->needs(LoggerInterface::class)
->give(FileLogger::class);
$container->when(UserService::class)
->needs(LoggerInterface::class)
->give(RemoteLogger::class);
服务提供者模式:模块化注册
当项目规模增长,把所有绑定都写在一个地方会导致配置文件臃肿。服务提供者(Service Provider)模式将绑定逻辑按功能模块组织,每个模块负责注册自己的服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 interface ServiceProviderInterface
{
public function register(Container $container): void;
}
class DatabaseServiceProvider implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->singleton(PDO::class, function (Container $c) {
$config = $c->make('config');
return new PDO(
$config->get('database.dsn'),
$config->get('database.username'),
$config->get('database.password'),
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
});
}
}
class CacheServiceProvider implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->singleton(CacheInterface::class, function (Container $c) {
$config = $c->make('config');
return new RedisCache(
$config->get('cache.host'),
$config->get('cache.port'),
$config->get('cache.database', 0)
);
});
$container->bind(CachePoolInterface::class, SymfonyCachePool::class);
}
}
class QueueServiceProvider implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->singleton(QueueInterface::class, RedisQueue::class);
$container->bind(WorkerInterface::class, QueueWorker::class);
$container->bind(JobDispatcherInterface::class, SyncJobDispatcher::class);
}
}
// 在应用启动时注册所有服务提供者
class Application
{
private Container $container;
public function __construct()
{
$this->container = new Container();
$providers = [
DatabaseServiceProvider::class,
CacheServiceProvider::class,
QueueServiceProvider::class,
// ... 更多提供者
];
foreach ($providers as $provider) {
(new $provider())->register($this->container);
}
}
public function getContainer(): Container
{
return $this->container;
}
}
服务提供者模式的另一个重要应用是实现延迟加载。并非所有服务在每次请求中都会用到,我们可以将服务注册推迟到真正需要的时候:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 class LazyServiceProvider implements ServiceProviderInterface
{
private static array $deferred = [
'image_processor' => ImageProcessor::class,
'pdf_generator' => PdfGenerator::class,
'email_sender' => EmailSender::class,
];
public function register(Container $container): void
{
foreach (self::$deferred as $key => $class) {
// 不立即创建,注册一个工厂闭包
$container->bind($key, function (Container $c) use ($class) {
return new $class(
$c->make(ConfigInterface::class)
);
});
}
}
}
PHP 8.x 特性加持:更优雅的依赖注入
PHP 8.x 引入的多项新特性,让依赖注入的实现和使用都更加优雅。以下是最值得关注的几个方面。
属性提升与只读属性
构造函数属性提升(Constructor Property Promotion)大幅减少了样板代码,配合
1 | readonly |
关键字可以确保注入的依赖不被意外修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 // PHP 7.x 的写法
class ReportService
{
private Repository $repository;
private Exporter $exporter;
private Validator $validator;
public function __construct(
Repository $repository,
Exporter $exporter,
Validator $validator
) {
$this->repository = $repository;
$this->exporter = $exporter;
$this->validator = $validator;
}
}
// PHP 8.1+ 的写法——简洁且安全
class ReportService
{
public function __construct(
private readonly Repository $repository,
private readonly Exporter $exporter,
private readonly Validator $validator,
) {}
}
命名参数与容器配置
命名参数让容器可以更灵活地处理非类型提示的参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 class Mailer
{
public function __construct(
private string $host,
private int $port,
private string $encryption = 'tls',
) {}
}
// 使用命名参数绑定
$container->bind(Mailer::class, function (Container $c) {
$config = $c->make('config');
return new Mailer(
host: $config->get('mail.host'),
port: $config->get('mail.port'),
encryption: $config->get('mail.encryption', 'tls'),
);
});
属性(Attributes)实现声明式注入
PHP 8.0 的 Attributes(属性)为声明式依赖注入提供了原生支持,可以在不修改构造函数的情况下声明注入需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 #[Attribute(Attribute::TARGET_PROPERTY)]
class Inject
{
public function __construct(
public ?string $interface = null
) {}
}
class DashboardController
{
#[Inject]
private UserService $users;
#[Inject(interface: CacheInterface::class)]
private $cache;
#[Inject]
private LoggerInterface $logger;
public function index(): Response
{
$stats = $this->users->getStatistics();
$this->cache->set('dashboard_stats', $stats, 300);
$this->logger->info('Dashboard loaded');
return new Response($stats);
}
}
// 容器端处理属性注入
class AttributeContainer extends Container
{
protected function injectAttributes(object $instance): void
{
$reflector = new ReflectionObject($instance);
foreach ($reflector->getProperties() as $property) {
$attributes = $property->getAttributes(Inject::class);
if (empty($attributes)) {
continue;
}
$inject = $attributes[0]->newInstance();
$type = $inject->interface ?? $property->getType()->getName();
$property->setAccessible(true);
$property->setValue($instance, $this->make($type));
}
}
}
实战:构建完整的服务容器系统
将前面学到的所有技术整合起来,我们可以构建一个适用于中大型项目的完整服务容器。以下是核心代码和关键设计决策:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103 class ServiceContainer
{
private array $bindings = [];
private array $instances = [];
private array $aliases = [];
private array $tags = [];
private bool $autowireEnabled = true;
/**
* 别名注册——用短名访问服务
*/
public function alias(string $alias, string $abstract): void
{
$this->aliases[$alias] = $abstract;
}
/**
* 标签分组——按功能批量解析服务
*/
public function tag(string $tag, array $services): void
{
$this->tags[$tag] = $services;
}
/**
* 解析标签下的所有服务
*/
public function tagged(string $tag): array
{
$services = [];
foreach ($this->tags[$tag] ?? [] as $abstract) {
$services[] = $this->make($abstract);
}
return $services;
}
/**
* 启用或禁用自动装配
*/
public function autowire(bool $enabled): void
{
$this->autowireEnabled = $enabled;
}
public function make(string $abstract): object
{
// 解析别名
$abstract = $this->aliases[$abstract] ?? $abstract;
// 单例缓存
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
$concrete = $this->bindings[$abstract] ?? null;
// 没有显式绑定,尝试自动装配
if ($concrete === null) {
if (!$this->autowireEnabled) {
throw new RuntimeException(
"No binding for {$abstract} and autowiring is disabled"
);
}
return $this->build($abstract);
}
// 闭包绑定
if (is_callable($concrete)) {
$instance = $concrete($this);
} else {
$instance = $this->build($concrete);
}
// 单例缓存
if (isset($this->bindings[$abstract]['singleton'])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
// ... build() 和 resolveDependencies() 同前 ...
}
// 使用示例
$container = new ServiceContainer();
// 注册服务
$container->singleton(PDO::class, fn($c) => new PDO(/* ... */));
$container->bind(UserRepository::class, MySqlUserRepository::class);
$container->alias('db', PDO::class);
// 标签分组——事件监听器
$container->tag('listeners', [
UserCreatedListener::class,
OrderPlacedListener::class,
PaymentFailedListener::class,
]);
// 批量解析标签下的服务
foreach ($container->tagged('listeners') as $listener) {
$dispatcher->addListener($listener);
}
依赖注入与单元测试的协同
依赖注入最大的实战价值之一,就是让单元测试变得简单而优雅。通过注入模拟对象(Mock),我们可以完全隔离被测单元,不需要真实的数据库、缓存或第三方服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 class OrderServiceTest extends TestCase
{
private OrderService $service;
private OrderRepository|MockObject $orders;
private PaymentGateway|MockObject $payment;
private NotificationService|MockObject $notifier;
protected function setUp(): void
{
// 创建模拟对象
$this->orders = $this->createMock(OrderRepository::class);
$this->payment = $this->createMock(PaymentGateway::class);
$this->notifier = $this->createMock(NotificationService::class);
// 通过构造函数注入模拟依赖
$this->service = new OrderService(
$this->orders,
$this->payment,
$this->notifier
);
}
public function testPlaceOrderSuccess(): void
{
$order = new Order(amount: 99.99, userId: 1);
// 设置模拟行为
$this->orders
->expects($this->once())
->method('save')
->with($order);
$this->payment
->expects($this->once())
->method('charge')
->with(99.99)
->willReturn(new PaymentResult(success: true));
$this->notifier
->expects($this->once())
->method('sendConfirmation')
->with($order);
// 执行测试
$result = $this->service->placeOrder($order);
$this->assertTrue($result->isSuccess());
}
public function testPlaceOrderPaymentFailed(): void
{
$order = new Order(amount: 99.99, userId: 1);
$this->orders->method('save')->willReturn(true);
$this->payment->method('charge')->willReturn(
new PaymentResult(success: false, error: 'Insufficient funds')
);
// 支付失败时不应发送通知
$this->notifier
->expects($this->never())
->method('sendConfirmation');
$result = $this->service->placeOrder($order);
$this->assertFalse($result->isSuccess());
}
}
如果没有依赖注入,测试
1 | OrderService |
就需要启动真实的数据库和支付网关——这不仅缓慢、不稳定,而且可能产生真实的费用。DI 让测试变得快速、可重复、零副作用。
常见陷阱与最佳实践
在使用依赖注入的过程中,有一些常见的陷阱需要警惕。以下是最重要的几条最佳实践:
避免服务定位器反模式
服务定位器(Service Locator)看起来和 DI 容器很像,但有一个关键区别:DI 是从外部传入依赖,而服务定位器是在类内部主动拉取依赖。后者把容器当成了全局变量,削弱了依赖关系的可见性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 // 反模式:服务定位器
class BadController
{
public function action(): Response
{
// 从容器中主动拉取——依赖关系被隐藏了
$db = Container::getInstance()->make(PDO::class);
$cache = Container::getInstance()->make(CacheInterface::class);
// ...
}
}
// 正确做法:构造函数注入
class GoodController
{
public function __construct(
private readonly PDO $db,
private readonly CacheInterface $cache,
) {}
public function action(): Response
{
// 依赖已经在构造函数中明确声明
}
}
不要注入容器本身
将整个容器注入到类中,等于放弃了依赖注入的所有优势——你无法从类签名看出它到底依赖什么,测试时也无法精确模拟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 // 反模式
class OrderService
{
public function __construct(private Container $container) {}
public function process(): void
{
// 隐藏依赖——外部无法知道这个类用了什么
$repo = $this->container->make(OrderRepository::class);
$mailer = $this->container->make(Mailer::class);
}
}
// 正确做法
// 只注入真正需要的依赖
$orderService = new OrderService($repo, $mailer);
控制单例的使用范围
单例模式在 DI 容器中是一把双刃剑。过度使用单例会导致状态在请求之间泄漏,增加调试难度。建议遵循以下原则:
- 无状态的服务(如数据库连接、配置对象)适合单例
- 有状态的对象(如用户会话、购物车)不应该用单例
- 当不确定时,默认使用非单例(
1bind
而非
1singleton)
利用编译时优化
在生产环境中,反射的性能开销是不可忽视的。大型框架(如 Symfony、Laravel)都提供了容器编译/缓存机制,将运行时的反射解析转化为预生成的 PHP 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 // 编译后的容器类(自动生成)
class CompiledContainer
{
private array $services = [];
public function getUserService(): UserService
{
if (isset($this->services['UserService'])) {
return $this->services['UserService'];
}
return $this->services['UserService'] = new UserService(
$this->getUserRepository(),
$this->getCache(),
$this->getLogger()
);
}
public function getUserRepository(): UserRepository
{
return $this->services['UserRepository'] ??= new MySqlUserRepository(
$this->getPDO()
);
}
// ... 预编译的工厂方法 ...
}
// 在生产环境启动时
$container = new CompiledContainer();
// 零反射开销,所有依赖关系已硬编码为方法调用
编译后的容器消除了所有反射调用,性能可以提升 5-10 倍。对于高流量生产环境,这是必不可少的优化手段。
主流框架中的依赖注入对比
不同的 PHP 框架对依赖注入的实现各有特色。了解这些差异有助于你在不同项目中选择最合适的方式。
| 框架 | 容器实现 | 自动装配 | 编译优化 | 特色功能 |
|---|---|---|---|---|
| Symfony | DependencyInjection 组件 | 默认开启 | 支持(compile()) | 服务标签、事件订阅者自动发现 |
| Laravel | Illuminate Container | 默认开启 | 部分(路由/配置缓存) | Facade 系统、上下文绑定 |
| Spiral | Spiral Core | 默认开启 | 支持(通过 RoadRunner) | 属性注入、拦截器 |
| PHP-DI | 独立容器 | 默认开启 | 不支持 | 属性注入(Annotations/Attributes) |
Symfony 的容器是最完善的——它支持编译时优化、服务标签、自动配置等高级特性,适合大型企业级项目。Laravel 的容器更注重开发体验,Facade 系统让服务访问非常便捷,但也容易滑向服务定位器反模式。PHP-DI 是一个独立的容器库,适合在非框架项目中引入 DI。
总结
依赖注入不是一个可有可无的”最佳实践”,而是构建可维护 PHP 应用的基础设施。从手动传入依赖到自动装配容器,从接口绑定到上下文注入,从服务提供者到编译优化——每一步都是为了让代码更加解耦、更加可测试、更加灵活。
核心要点回顾:
- 优先使用构造函数注入,保证依赖的完整性和可见性
- 面向接口编程,通过容器绑定实现与具体实现解耦
- 利用 PHP 8.x 特性(属性提升、只读属性、Attributes)简化注入代码
- 服务提供者模式管理复杂项目的依赖注册
- 警惕服务定位器反模式,永远不要在类内部主动拉取依赖
- 生产环境使用编译容器消除反射开销
掌握依赖注入,不只是学会使用一个容器——而是掌握一种编写松耦合、可测试、可维护代码的思维方式。当你习惯了这种思维方式,你会发现代码的组织方式发生了根本性的改变:类与类之间通过契约协作,模块与模块之间通过接口通信,整个系统变得灵活而健壮。
汤不热吧