依赖自动注入
在webman里依赖自动注入是可选功能,此功能默认关闭。如果你需要依赖自动注入,推荐使用php-di,以下是webman结合php-di
的用法。
安装
composer require php-di/php-di:^7.0
修改配置config/container.php
,其最终内容如下:
$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAttributes(true);
return $builder->build();
config/container.php
里最终返回一个符合PSR-11
规范的容器实例。如果你不想使用php-di
,可以在这里创建并返回一个其它符合PSR-11
规范的容器实例。默认配置仅提供webman基础的容器功能。
构造函数注入
新建app/service/Mailer.php
(如目录不存在请自行创建)内容如下:
<?php
namespace app\service;
class Mailer
{
public function mail($email, $content)
{
// 发送邮件代码省略
}
}
app/controller/UserController.php
内容如下:
<?php
namespace app\controller;
use support\Request;
use app\service\Mailer;
class UserController
{
public function __construct(private Mailer $mailer)
{
}
public function register(Request $request)
{
$this->mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
正常情况下,需要以下代码才能完成app\controller\UserController
的实例化:
$mailer = new Mailer;
$user = new UserController($mailer);
当使用php-di
后,开发者无需手动实例化控制器中的Mailer
,webman会自动帮你完成。如果在实例化Mailer
过程中有其它类的依赖,webman也会自动实例化并注入。开发者不需要任何的初始化工作。
注意
必须是由框架或者php-di
创建的实例才能完成依赖自动注入,手动new
的实例无法完成依赖自动注入,如需注入,需要使用support\Container
接口替换new
语句,例如:
use app\service\UserService;
use app\service\LogService;
use support\Container;
// new关键字创建的实例无法依赖注入
$user_service = new UserService;
// new关键字创建的实例无法依赖注入
$log_service = new LogService($path, $name);
// Container创建的实例可以依赖注入
$user_service = Container::get(UserService::class);
// Container创建的实例可以依赖注入
$log_service = Container::make(LogService::class, [$path, $name]);
注解注入
除了构造函数依赖自动注入,我们还可以使用注解注入。继续上面的例子,app\controller\UserController
更改成如下:
<?php
namespace app\controller;
use support\Request;
use app\service\Mailer;
use DI\Attribute\Inject;
class UserController
{
#[Inject]
private Mailer $mailer;
public function register(Request $request)
{
$this->mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
这个例子使用 #[Inject]
注解注入,并且通过对象类型自动将实例注入到成员变量中。效果与构造函数注入一样,但是代码更精简。
注意
webman在1.4.6版本之前不支持控制器参数注入,例如以下代码当webman<=1.4.6时是不支持的
<?php
namespace app\controller;
use support\Request;
use app\service\Mailer;
class UserController
{
// 1.4.6版本之前不支持控制器参数注入
public function register(Request $request, Mailer $mailer)
{
$mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
自定义构造函数注入
有时候构造函数传入的参数可能不是类的实例,而是字符串、数字、数组等非object数据。例如Mailer构造函数需要传递smtp服务器ip和端口:
<?php
namespace app\service;
class Mailer
{
private $smtpHost;
private $smtpPort;
public function __construct($smtp_host, $smtp_port)
{
$this->smtpHost = $smtp_host;
$this->smtpPort = $smtp_port;
}
public function mail($email, $content)
{
// 发送邮件代码省略
}
}
这种情况无法直接使用前面介绍的构造函数自动注入,因为php-di
无法确定$smtp_host
$smtp_port
的值是什么。这时候可以尝试自定义注入。
在config/dependence.php
(文件不存在请自行创建)中加入如下代码:
return [
// ... 这里忽略了其它配置
app\service\Mailer::class => new app\service\Mailer('192.168.1.11', 25);
];
这样当依赖注入需要获取app\service\Mailer
实例时将自动使用这个配置中创建的app\service\Mailer
实例。
我们注意到,config/dependence.php
中使用了new
来实例化Mailer
类,这个在本示例没有任何问题,但是想象下如果Mailer
类依赖了其它类的话或者Mailer
类内部使用了注解注入,使用new
初始化将不会依赖自动注入。解决办法是利用自定义接口注入,通过Container::get(类名)
或者 Container::make(类名, [构造函数参数])
方法来初始化类。
自定义接口注入
在现实项目中,我们更希望面向接口编程,而不是具体的类。比如app\controller\UserController
里应该引入app\service\MailerInterface
而不是app\service\Mailer
。
定义MailerInterface
接口。
<?php
namespace app\service;
interface MailerInterface
{
public function mail($email, $content);
}
定义MailerInterface
接口的实现。
<?php
namespace app\service;
class Mailer implements MailerInterface
{
private $smtpHost;
private $smtpPort;
public function __construct($smtp_host, $smtp_port)
{
$this->smtpHost = $smtp_host;
$this->smtpPort = $smtp_port;
}
public function mail($email, $content)
{
// 发送邮件代码省略
}
}
引入MailerInterface
接口而非具体实现。
<?php
namespace app\controller;
use support\Request;
use app\service\MailerInterface;
use DI\Attribute\Inject;
class UserController
{
#[Inject]
private MailerInterface $mailer;
public function register(Request $request)
{
$this->mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
config/dependence.php
将 MailerInterface
接口定义如下实现。
use Psr\Container\ContainerInterface;
return [
app\service\MailerInterface::class => function(ContainerInterface $container) {
return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
}
];
这样当业务需要使用MailerInterface
接口时,将自动使用Mailer
实现。
面向接口编程的好处是,当我们需要更换某个组件时,不需要更改业务代码,只需要更改
config/dependence.php
中的具体实现即可。这在做单元测试也非常有用。
其它自定义注入
config/dependence.php
除了能定义类的依赖,也能定义其它值,例如字符串、数字、数组等。
例如config/dependence.php
定义如下:
return [
'smtp_host' => '192.168.1.11',
'smtp_port' => 25
];
这时候我们可以通过#[Inject]
将smtp_host
smtp_port
注入到类的属性中。
<?php
namespace app\service;
use DI\Attribute\Inject;
class Mailer
{
#[Inject("smtp_host")]
private $smtpHost;
#[Inject("smtp_port")]
private $smtpPort;
public function mail($email, $content)
{
// 发送邮件代码省略
echo "{$this->smtpHost}:{$this->smtpPort}\n"; // 将输出 192.168.1.11:25
}
}
延迟加载
延迟加载是一种设计模式,用于推迟对象的创建或初始化,直到实际需要使用时才进行加载。
使用此功能需要额外安装依赖,以下依赖为ocramius/proxy-manager
的一个分支,原仓库不支持PHP8。
composer require friendsofphp/proxy-manager-lts
使用方法:
<?php
use DI\Attribute\Injectable;
use DI\Attribute\Inject;
#[Injectable(lazy: true)]
class MyClass
{
private string $name;
public function __construct()
{
echo "MyClass 实例化\n";
$this->name = "Lazy Loaded Object";
}
public function getName(): string
{
return $this->name;
}
}
class Controller
{
#[Inject]
public MyClass $myClass;
public function getClass()
{
echo "代理对象类名: " . get_class($this->myClass) . "\n";
echo "name: " . $this->myClass->getName();
}
}
输出:
代理对象类名: ProxyManagerGeneratedProxy\__PM__\app\web\MyClass\Generated98d2817da63e3c088c808a0d4f6e9ae0
MyClass 实例化
name: Lazy Loaded Object
以上示例说明了声明#[Injectable]
注解的类在被注入时首先会创建此类的代理类,只有任意方法被调用后才会实例化。
循环依赖
循环依赖是指多个类中相互依赖,形成一个闭环依赖关系。
- 直接循环依赖
- 模块A依赖模块B,模块B又依赖模块A
- 形成 A → B → A 的依赖闭环
- 间接循环依赖
- 涉及多个模块形成的依赖环
- 如 A → B → C → A 的情况
在使用注解注入时php-di
会自动检测到循环依赖并抛出异常,如有需要请使用以下代码代替
class userController
{
// 移除这行代码
// #[Inject]
// private UserService userService;
public function getUserName()
{
$userService = Container::get(UserService::class);
return $userService->getName();
}
}
更多内容
请参考php-di手册