欢迎光临

PHP 依赖注入与容器实战:从手动注入到自动装配的完整架构指南

什么是依赖注入:从硬编码到松耦合的演进

在 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 容器中是一把双刃剑。过度使用单例会导致状态在请求之间泄漏,增加调试难度。建议遵循以下原则:

  • 无状态的服务(如数据库连接、配置对象)适合单例
  • 有状态的对象(如用户会话、购物车)不应该用单例
  • 当不确定时,默认使用非单例(
    1
    bind

    而非

    1
    singleton

利用编译时优化

在生产环境中,反射的性能开销是不可忽视的。大型框架(如 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)简化注入代码
  • 服务提供者模式管理复杂项目的依赖注册
  • 警惕服务定位器反模式,永远不要在类内部主动拉取依赖
  • 生产环境使用编译容器消除反射开销

掌握依赖注入,不只是学会使用一个容器——而是掌握一种编写松耦合、可测试、可维护代码的思维方式。当你习惯了这种思维方式,你会发现代码的组织方式发生了根本性的改变:类与类之间通过契约协作,模块与模块之间通过接口通信,整个系统变得灵活而健壮。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » PHP 依赖注入与容器实战:从手动注入到自动装配的完整架构指南
分享到: 更多 (0)