依赖自动注入

在webman里依赖自动注入是可选功能,此功能默认关闭。如果你需要依赖自动注入,推荐使用php-di,以下是webman结合php-di的用法。

安装

  1. composer require psr/container ^1.1.1 php-di/php-di doctrine/annotations

修改配置config/container.php,其最终内容如下:

  1. $builder = new \DI\ContainerBuilder();
  2. $builder->addDefinitions(config('dependence', []));
  3. $builder->useAutowiring(true);
  4. $builder->useAnnotations(true);
  5. return $builder->build();

config/container.php里最终返回一个符合PSR-11规范的容器实例。如果你不想使用 php-di ,可以在这里创建并返回一个其它符合PSR-11规范的容器实例。

构造函数注入

新建app/service/Mailer.php(如目录不存在请自行创建)内容如下:

  1. <?php
  2. namespace app\service;
  3. class Mailer
  4. {
  5. public function mail($email, $content)
  6. {
  7. // 发送邮件代码省略
  8. }
  9. }

app\controller\User.php内容如下:

  1. <?php
  2. namespace app\controller;
  3. use support\Request;
  4. use app\service\Mailer;
  5. class User
  6. {
  7. private $mailer;
  8. public function __construct(Mailer $mailer)
  9. {
  10. $this->mailer = $mailer;
  11. }
  12. public function register(Request $request)
  13. {
  14. $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
  15. return response('ok');
  16. }
  17. }

正常情况下,需要以下代码才能完成app\controller\User的实例化:

  1. $mailer = new Mailer;
  2. $user = new User($mailer);

当使用php-di后,开发者无需手动实例化Mailer,webman会自动帮你完成。如果在实例化Mailer过程中有其它类的依赖,webman也会自动实例化并注入。开发者不需要任何的初始化工作。

注意:必须是由php-di创建的实例才能完成依赖自动注入,手动new的实例无法完成依赖自动注入。controller是由webman通过php-di实例化的,所以直接支持依赖自动注入。如果其它类想使用依赖自动注入,可以调用\support\Container::get(类名)或者\support\Container::make(类名, [构造函数参数])来创建对应类的实例。例如:

  1. use app\service\UserService;
  2. use app\service\LogService;
  3. use support\Container;
  4. $user_service = Container::get(UserService::class);
  5. $log_service = Container::make(LogService::class, [$path, $name]);

注解注入

除了构造函数依赖自动注入,我们还可以使用注解注入。继续上面的例子,app\controller\User更改成如下:

  1. <?php
  2. namespace app\controller;
  3. use support\Request;
  4. use app\service\Mailer;
  5. use DI\Annotation\Inject;
  6. class User
  7. {
  8. /**
  9. * @Inject
  10. * @var Mailer
  11. */
  12. private $mailer;
  13. public function register(Request $request)
  14. {
  15. $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
  16. return response('ok');
  17. }
  18. }

这个例子通过 @Inject 注解注入,并且由 @var 注解声明对象类型。这个例子和构造函数注入效果一样,但是代码更精简。

注意 webman目前暂不支持控制器参数注入,例如以下代码是不支持的

  1. <?php
  2. namespace app\controller;
  3. use support\Request;
  4. use app\service\Mailer;
  5. class User
  6. {
  7. // 不支持控制器参数注入
  8. public function register(Request $request, Mailer $mailer)
  9. {
  10. $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
  11. return response('ok');
  12. }
  13. }

自定义构造函数注入

有时候构造函数传入的参数可能不是类的实例,而是字符串、数字、数组等数据。例如Mailer构造函数需要传递smtp服务器ip和端口:

  1. <?php
  2. namespace app\service;
  3. class Mailer
  4. {
  5. private $smtpHost;
  6. private $smtpPort;
  7. public function __construct($smtp_host, $smtp_port)
  8. {
  9. $this->smtpHost = $smtp_host;
  10. $this->smtpPort = $smtp_port;
  11. }
  12. public function mail($email, $content)
  13. {
  14. // 发送邮件代码省略
  15. }
  16. }

这种情况无法直接使用前面介绍的构造函数自动注入,因为php-di无法确定$smtp_host $smtp_port的值是什么。这时候可以尝试自定义注入。

config/dependence.php(文件不存在请自行创建)中加入如下代码:

  1. return [
  2. // ... 这里忽略了其它配置
  3. app\service\Mailer::class => new app\service\Mailer('192.168.1.11', 25);
  4. ];

这样当依赖注入需要获取app\service\Mailer实例时将自动使用这个配置中创建的app\service\Mailer实例。

我们注意到,config/dependence.php 中使用了new来实例化Mailer类,这个在本示例没有任何问题,但是想象下如果Mailer类依赖了其它类的话或者Mailer类内部使用了注解注入,使用new初始化将不会依赖自动注入。解决办法是利用自定义接口注入,通过Container::get(类名) 或者 Container::make(类名, [构造函数参数])方法来初始化类。

自定义接口注入

在现实项目中,我们更希望面向接口编程,而不是具体的类。比如app\controller\User里应该引入app\service\MailerInterface而不是app\service\Mailer

定义MailerInterface接口。

  1. <?php
  2. namespace app\service;
  3. interface MailerInterface
  4. {
  5. public function mail($email, $content);
  6. }

定义MailerInterface接口的实现。

  1. <?php
  2. namespace app\service;
  3. class Mailer implements MailerInterface
  4. {
  5. private $smtpHost;
  6. private $smtpPort;
  7. public function __construct($smtp_host, $smtp_port)
  8. {
  9. $this->smtpHost = $smtp_host;
  10. $this->smtpPort = $smtp_port;
  11. }
  12. public function mail($email, $content)
  13. {
  14. // 发送邮件代码省略
  15. }
  16. }

引入MailerInterface接口而非具体实现。

  1. <?php
  2. namespace app\controller;
  3. use support\Request;
  4. use app\service\MailerInterface;
  5. use DI\Annotation\Inject;
  6. class User
  7. {
  8. /**
  9. * @Inject
  10. * @var MailerInterface
  11. */
  12. private $mailer;
  13. public function register(Request $request)
  14. {
  15. $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
  16. return response('ok');
  17. }
  18. }

config/dependence.phpMailerInterface 接口定义如下实现。

  1. use Psr\Container\ContainerInterface;
  2. return [
  3. app\service\MailerInterface::class => function(ContainerInterface $container) {
  4. return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
  5. }
  6. ];

这样当业务需要使用MailerInterface接口时,将自动使用Mailer实现。

面向接口编程的好处是,当我们需要更换某个组件时,不需要更改业务代码,只需要更改config/dependence.php中的具体实现即可。这在做单元测试也非常有用。

其它自定义注入

config/dependence.php除了能定义类的依赖,也能定义其它值,例如字符串、数字、数组等。

例如config/dependence.php定义如下:

  1. return [
  2. 'smtp_host' => '192.168.1.11',
  3. 'smtp_port' => 25
  4. ];

这时候我们可以通过@Injectsmtp_host smtp_port 注入到类的属性中。

  1. <?php
  2. namespace app\service;
  3. use DI\Annotation\Inject;
  4. class Mailer
  5. {
  6. /**
  7. * @Inject("smtp_host")
  8. */
  9. private $smtpHost;
  10. /**
  11. * @Inject("smtp_port")
  12. */
  13. private $smtpPort;
  14. public function mail($email, $content)
  15. {
  16. // 发送邮件代码省略
  17. echo "{$this->smtpHost}:{$this->smtpPort}\n"; // 将输出 192.168.1.11:25
  18. }
  19. }

注意:@Inject("key") 里面是双引号

更多内容

请参考php-di手册