注入类型
显式地声明一个类的依赖,并要求将它们注入到该类中,是使类更可重用、可测试和与其他类解耦的好方法。
有几种方法可以注入依赖。每种注入点都有其优缺点需要考虑,以及在使用服务容器时不同的工作方式。
构造器注入
注入依赖最常见的方式是通过类的构造器。为此,您需要在构造器签名中添加一个参数来接受依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13
// src/Mail/NewsletterManager.php
namespace App\Mail;
// ...
class NewsletterManager
{
public function __construct(
private MailerInterface $mailer,
) {
}
// ...
}
您可以在服务容器配置中指定要注入到此构造器中的服务。
1 2 3 4 5 6
# config/services.yaml
services:
# ...
App\Mail\NewsletterManager:
arguments: ['@mailer']
提示
类型提示注入的对象意味着您可以确保已注入合适的依赖。通过类型提示,如果注入了不合适的依赖,您将立即收到明确的错误。通过使用接口而不是类进行类型提示,您可以使依赖的选择更加灵活。并且假设您只使用接口中定义的方法,您就可以获得这种灵活性,并且仍然可以安全地使用该对象。
使用构造器注入有几个优点:
- 如果依赖是必需的,并且类在没有它就无法工作,那么通过构造器注入可以确保在类被使用时它存在,因为没有它就无法构造类。
- 构造器仅在创建对象时调用一次,因此您可以确保依赖在对象的生命周期内不会更改。
这些优点意味着构造器注入不适用于处理可选依赖。与类继承结合使用也更困难:如果一个类使用构造器注入,那么扩展它并覆盖构造器就会变得有问题。
不可变 Setter 注入
另一种可能的注入是使用一种方法,该方法通过克隆原始服务返回一个单独的实例,这种方法允许您使服务成为不可变的。
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
// src/Mail/NewsletterManager.php
namespace App\Mail;
// ...
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Service\Attribute\Required;
class NewsletterManager
{
private MailerInterface $mailer;
/**
* @return static
*/
#[Required]
public function withMailer(MailerInterface $mailer): self
{
$new = clone $this;
$new->mailer = $mailer;
return $new;
}
// ...
}
为了使用这种类型的注入,请不要忘记配置它。
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# ...
app.newsletter_manager:
class: App\Mail\NewsletterManager
calls:
- withMailer: !returns_clone ['@mailer']
注意
如果您决定使用自动装配,则这种类型的注入需要您添加 @return static
docblock 或 static
返回类型,以便容器能够注册该方法。
如果您需要根据自己的需求配置服务,则此方法非常有用,因此,以下是不可变 Setter 的优点:
- 不可变 Setter 适用于可选依赖,这样,如果您不需要依赖,则无需调用 Setter。
- 与构造器注入一样,使用不可变 Setter 可以强制依赖在服务的生命周期内保持不变。
- 这种类型的注入与 Trait 配合良好,因为服务可以被组合,这样,根据您的应用程序需求调整服务变得更加容易。
- Setter 可以被多次调用,这样,向集合中添加依赖变得更容易,并允许您添加可变数量的依赖。
缺点是:
- 由于 Setter 调用是可选的,因此在调用服务的方法时,依赖可能为 null。您必须在使用依赖之前检查依赖是否可用。
- 除非服务声明为延迟加载,否则它与在所谓的循环依赖中相互引用的服务不兼容。
Setter 注入
注入类的另一个可能的注入点是通过添加一个接受依赖的 Setter 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Mail/NewsletterManager.php
namespace App\Mail;
use Symfony\Contracts\Service\Attribute\Required;
// ...
class NewsletterManager
{
private MailerInterface $mailer;
#[Required]
public function setMailer(MailerInterface $mailer): void
{
$this->mailer = $mailer;
}
// ...
}
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# ...
app.newsletter_manager:
class: App\Mail\NewsletterManager
calls:
- setMailer: ['@mailer']
这次的优点是:
- Setter 注入适用于可选依赖。如果您不需要依赖,则不要调用 Setter。
- 您可以多次调用 Setter。如果该方法将依赖添加到集合中,则这尤其有用。然后,您可以拥有可变数量的依赖。
- 与不可变 Setter 类似,这种类型的注入与 Trait 配合良好,并允许您组合您的服务。
Setter 注入的缺点是:
- Setter 可以被多次调用,甚至在初始化之后很久,因此您无法确定依赖在对象的生命周期内不会被替换(除非显式编写 Setter 方法来检查它是否已被调用)。
- 您无法确定 Setter 是否会被调用,因此您需要添加检查以确保注入任何必需的依赖。
属性注入
另一种可能性是直接设置类的公共字段。
1 2 3 4 5 6 7
// ...
class NewsletterManager
{
public MailerInterface $mailer;
// ...
}
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# ...
app.newsletter_manager:
class: App\Mail\NewsletterManager
properties:
mailer: '@mailer'
使用属性注入主要只有缺点,它类似于 Setter 注入,但存在这个额外的严重问题:
- 您完全无法控制何时设置依赖,它可以在对象生命周期的任何时候被更改。
但是,了解可以使用服务容器完成此操作很有用,特别是当您正在处理不受您控制的代码时,例如在第三方库中,该库对其依赖使用公共属性。