andy-shea / pulp
基于Google的Guice的PHP依赖注入框架
Requires
- php: >=8.0
Requires (Dev)
- mikey179/vfsstream: ^1.6
- phpunit/phpunit: ^9.5
README
最新版本: 2.0.0
持续集成:
需求: PHP 8+
Pulp为您处理对象图的繁琐连接,使您的代码更容易更改、测试和重用。将Pulp的Inject视为新的new。
入门指南
使用Pulp的最简单方法是使用Composer
composer require andy-shea/pulp
从这里开始,通过一个简单的示例来展示Pulp的使用是最好的。一个安全服务在应用中验证和授权用户是一个常见需求。这个安全服务可以依赖于身份验证策略和访问控制列表来执行相应的任务
class SecurityService { protected $strategy; protected $acl; public function __construct( AuthenticationStrategy $strategy, AccessControlList $acl ) { $this->strategy = $strategy; $this->acl = $acl; } public authenticateUser($username, $password) { return $this->strategy->autheticate($username, $password); } public authoriseUser(User $user, Resource $resource) { return $this->acl->isAllowed($user, $resource); } }
我们想通过传递一个AuthenticationStrategy和AccessControlList实现来创建一个SecurityService,以便它可以完成其身份验证和授权的角色。可以通过Module配置Pulp对此对象图的理解,这是Injector的构建块
public class SecurityModule extends AbstractModule { protected void configure() { $this->bind(AuthenticationStrategy::class)->to(BasicAuthStrategy::class); $this->bind(AccessControlList::class)->to(AdminAcl::class); } }
这告诉Pulp在需要AuthenticationStrategy类时返回一个BasicAuthStrategy实例,同样,当需要AccessControlList时,将实现一个AdminAcl。
我们还需要让Pulp知道它应该注入依赖的方法。在这里,SecurityService的构造函数需要这两个依赖项,所以我们用Inject属性标记它
class SecurityService { #[Inject] public function __construct( AuthenticationStrategy $strategy, AccessControlList $acl ) { ... } }
最后,定义了一个模块后,我们可以构建一个Injector来创建我们的SecurityService
$injectorBuilder = new InjectorBuilder(); $modules = [new SecurityModule()]; $injector = $injectorBuilder->addModules($modules)->build(); $securityService = $injector->getInstance('SecurityService')
绑定
为了使Injector正确执行其工作,它需要了解应用的对象图。这可以通过向Injector提供Binding来配置。
关联绑定
关联绑定将类型映射到其实例。在这里,接口AuthenticationStrategy映射到BasicAuthStrategy实现
$this->bind(AuthenticationStrategy::class)->to(BasicAuthStrategy::class);
现在当Injector遇到对AuthenticationStrategy的依赖时,它将使用一个BasicAuthStrategy。您可以从一个类型链接到其任何子类型,例如实现类或扩展类。您甚至可以将具体的BasicAuthStrategy类链接到子类
$this->bind(BasicAuthStrategy::class)->to(PersistentBasicAuthStrategy::class);
实例绑定
可以将类型的实际实例直接绑定。这通常只对没有自己依赖的对象(如值对象和基本类型)或通过其他方式创建的对象有用
$basicAuthStrategy = new BasicAuthStrategy(); $this->bind(AuthenticationStrategy::class)->toInstance($basicAuthStrategy); $this->bind('dbConnectionString')->toInstance('pgsql:host=localhost;port=5432;dbname=anotherdb');
注意:第二个示例突出了绑定到参数名称而不是类型的可能性。这可以用于解决原始依赖。
提供者绑定
当您需要执行更多工作来创建对象时,请使用Provides方法。此方法必须在模块内定义,并且必须有一个具有相应绑定类型的Provides属性。每次需要该类型的实例时,都会调用该方法并注入返回的对象
public class SecurityModule extends AbstractModule { protected void configure() { ... } #[Provides(Database::class)] public function provideDatabase() { $db = new PostgresDatabase('pgsql:host=localhost;port=5432;dbname=testdb'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $db; } }
当创建对象的工作比单个方法处理更复杂时,将代码移动到提供者类中开始更有意义。Pulp为此类提供了一个Provider接口来实现
interface Provider { function get(); }
提供者实现可以接收自己的依赖。只需像对任何其他类一样提供Inject属性即可
public class DatabaseProvider implements Provider { protected $dbLog; #[Inject] public __construct(Log $dbLog) { $this->dbLog = $dbLog; } public function get() { $db = new PostgresDatabase('pgsql:host=localhost;port=5432;dbname=testdb'); $db->setLog($this->dbLog); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $db; } }
最后,我们使用toProvider方法绑定到提供者
public class SecurityModule extends AbstractModule { protected void configure() { $this->bind(Database::class)->toProvider(DatabaseProvider::class); } }
Pulp还提供提供者的自动生成。这在创建依赖项时虽然不复杂,但成本高昂,需要延迟加载依赖项,或者需要打破循环依赖链时非常有用
class BillingService { protected $paymentProcessorProvider; #[Inject] public function __construct(#[Provides(CreditCardProcessor::class)] Provider $paymentProcessorProvider) { $this->paymentProcessorProvider = $paymentProcessorProvider; } }
在这里,我们不是直接传递CreditCardProcessor,而是通过Provides属性通知Pulp需要一个在必要时可以返回所需类型的提供者。Pulp会自动创建一个提供者,该提供者在被调用时返回一个CreditCardProcessor。
工厂是创建在运行时确定的多个对象家族中对象的常见模式。然而,编写工厂的代码既重复又脆弱。Pulp通过自动从给定的工厂接口生成它们来免除编写工厂实现代码的需求。给定两种支付类型
class CreditCardPayment implements Payment { #[Inject] public function __construct(MerchantGateway $gateway) { ... } } class CashPayment implements Payment { ... }
和一个工厂接口
interface PaymentFactory { #[Returns(CreditCardPayment::class)] function createCreditCardPayment(); #[Returns(CashPayment::class)] function createCashPayment(); }
使用PaymentFactory接口创建FactoryProvider并将其安装到Module中
public class PaymentModule extends AbstractModule { protected void configure() { $this->install(new FactoryProvider('PaymentFactory')); } }
Pulp可以自动创建和注入PaymentFactory实现以构建所需的支付
class CashDrawer { protected $paymentFactory; #[Inject] public function __construct(PaymentFactory $paymentFactory) { $this->paymentFactory = $paymentFactory; } public function addCreditCardPayment($amount) { $payment = $this->paymentFactory->createCreditCardPayment(); ... } }
工厂接口中的每个方法都必须使用Returns注解,并附上由方法创建的相应类型。
从自动生成的工厂实现返回的对象也将有它们的依赖项注入。在上面的例子中,当调用createCreditCardPayment()时,返回的CreditCardPayment将具有其MerchantGateway已解决。
注解绑定
偶尔,您会遇到需要相同类型的不同变体的情况。为了解决这个问题,Pulp提供了Qualifier和Named属性作为注解类型的方法。例如,您的应用程序可能需要与多个数据库源交互
class ClientSecurityService { #[Inject] public function __construct(Database $clientDatabase) { .. } } class AdminSecurityService { #[Inject] public function __construct(Database $adminDatabase) { .. } }
在这种情况下,Pulp无法在绑定依赖项时区分这两个数据库。但是,通过注解,我们可以提供不同的绑定目标来解析依赖项。对于限定符,首先创建表示类型差异的新Qualifier属性
#[Attribute(Attribute::TARGET_PARAMETER), Qualifier] final class ClientDatabase {} #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER), Qualifier] final class AdminDatabase {}
然后注解所需的参数或属性,使用新的Qualifier属性
class ClientSecurityService { #[Inject] public function __construct(#[ClientDatabase] Database $db) { .. } } class AdminSecurityService { #[Inject, AdminDatabase] protected Database $db; }
最后,在模块中绑定限定符
public class SecurityModule extends AbstractModule { protected void configure() { $this->bind(ClientDatabase::class)->toProvider(ClientDatabaseProvider::class); $this->bind(AdminDatabase::class)->toProvider(AdminDatabaseProvider::class); } }
或者,可以使用Named属性实现这一点
class ClientSecurityService { #[Inject] public function __construct(#[Named('ClientDatabase')] Database $db) { .. } } class AdminSecurityService { #[Inject, Named('AdminDatabase')] protected Database $db; } public class SecurityModule extends AbstractModule { protected void configure() { $this->bind('ClientDatabase')->toProvider(ClientDatabaseProvider::class); $this->bind('AdminDatabase')->toProvider(AdminDatabaseProvider::class); } }
隐式绑定
需要注意的是,并非所有依赖项都需要显式绑定。如果一个对象依赖于一个具体类,且没有为此类提供显式绑定,Pulp将自动注入该具体类的实例。这被称为隐式绑定。
有时需要的重要隐式绑定是Injector本身。在框架代码中,有时您不知道在运行时需要什么类型。在这种情况下,您应该注入注入器。注入注入器的代码没有自我记录其依赖项,因此应谨慎使用此方法。
作用域
默认情况下,Pulp在每次实现依赖项时返回一个新的实例。如果需要更多灵活性,Pulp提供Scope来配置此行为。在Module中,可以使用Scope进一步配置绑定
$this->bind(AuthenticationStrategy::class)->to(BasicAuthStrategy::class)->in(Scopes::singleton());
绑定不需要目标进行作用域配置。要指定具体类的范围,可以使用无目标绑定
$this->bind(RouteMapper::class)->in(Scopes::singleton());
提供者方法也可以配置作用域
#[Provides(Database::class), Singleton] public function provideDatabase() { ... }
注入
依赖注入模式将行为与依赖项解析分离。而不是直接查找依赖项或从工厂中查找,该模式建议传递依赖项。将依赖项设置到对象中的过程称为注入。
属性、构造函数和方法注入
Pulp注入任何在注解了Inject的类上定义的属性、方法或构造函数
class SecurityService { protected $strategy; protected $acl; protected $log; #[Inject] protected EmailService $emailService; #[Inject] public function __construct( AuthenticationStrategy $strategy, AccessControlList $acl ) { $this->strategy = $strategy; $this->acl = $acl; } #[Inject] public function setLog(Log $securityLog) { $this->log = $securityLog; } public authenticateUser($username, $password) { return $this->strategy->autheticate($username, $password); } public authoriseUser(User $user, Resource $resource) { return $this->acl->isAllowed($user, $resource); } }
可选注入
在注入方法或构造函数中的所有依赖项都必须可解析,否则将抛出异常。对此规则的例外是已定义默认值的参数;在这些情况下,Pulp 将自动回退到提供的默认值。
class PostgresDatabase { #[Inject] public function __construct( #[Named('adminConnectionString')] $connectionString = 'pgsql:host=localhost;port=5432;dbname=testdb' ) { ... } }
这里没有找到对 adminConnectionString 的绑定,因此 Pulp 将注入默认值 pgsql:host=localhost;port=5432;dbname=testdb。
辅助注入
有时,依赖项将需要只能由父对象提供的参数。解决此问题的典型模式是提供一个工厂,该工厂知道如何在传入给定参数时创建对象。如上所述在提供者绑定部分,Pulp 通过自动为您生成描述创建对象所需合约的工厂接口来解决这个问题。这里的区别是,我们现在需要指定需要父对象贡献的参数,以便 Pulp 可以正确构建工厂方法。为了扩展先前的示例,我们的支付类型需要在它们的构造函数中传递一个金额。
class CreditCardPayment implements Payment { #[Inject] public function __construct(MerchantGateway $gateway, #[Assisted] $amount) { ... } } class CashPayment implements Payment { #[Inject] public function __construct(#[Assisted] $amount) { ... } }
注意标记需要手动贡献的参数的 Assisted 属性。工厂接口现在需要在它的创建方法中定义匹配的参数。
interface PaymentFactory { #[Returns(CreditCardPayment::class)] function createCreditCardPayment($amount); #[Returns(CashPayment::class)] function createCashPayment($amount); } public class PaymentModule extends AbstractModule { protected void configure() { $this->install(new FactoryProvider('PaymentFactory')); } }
在创建 CreditCardPayment 时,Pulp 仍然会自动注入 MerchantGateway 依赖项,但需要调用者传递金额。
class CashDrawer { protected $paymentFactory; #[Inject] public function __construct(PaymentFactory $paymentFactory) { $this->paymentFactory = $paymentFactory; } public function addCreditCardPayment($amount) { $payment = $this->paymentFactory->createCreditCardPayment($amount); ... } }