kelvinmo / lightcontainer
一个轻量级、自动装配、PSR-11兼容的容器
Requires
- php: ^7.2 || ^8.0
- psr/container: ^1.1 || ^2.0
Requires (Dev)
- phpstan/phpstan: ^1.3
- phpunit/phpunit: ^8.0 || ^9.0
Provides
README
LightContainer 是一个简单的 PSR-11 兼容的依赖注入容器,支持自动装配。
目录
要求
- PHP 7.2 或更高版本
安装
您可以通过 Composer 安装。
composer require kelvinmo/lightcontainer
基本用法
考虑以下一系列类
class A {} class B { public function __construct(A $a) {} } class C { public function __construct(B $b) {} }
通常,如果您想创建 C
,您需要执行以下操作
$c = new C(new B(new A()));
但是,使用 LightContainer 您可以这样做
$container = new LightContainer\Container(); $c = $container->get(C::class);
请注意,无需对容器进行任何配置。LightContainer 根据构造函数参数提供的类型提示确定依赖关系,然后配置自身以创建必要的对象。这是 自动装配。
有关自动装配如何工作的更多详细信息,请参阅参考。
配置
介绍
您可以通过调用 set
方法来配置容器。此方法可以带有最多两个参数:一个字符串 条目标识符 和一个可选值。您可以使用此方法的几种主要方式在本节中概述,更多详细信息可以在下面的 参考 部分找到。
实例化选项
您可以通过调用带有类名的 set
方法来为特定类设置实例化选项。这将返回一个 类解析器 对象,然后允许您指定选项。
$container->set(D::class)->shared();
可以通过链式调用方法来设置多个选项。
$container->set(D::class) ->alias(FooInterface::class, FooInterfaceImpl::class) ->shared();
可以使用属性设置某些实例化选项。
每次您在容器对象上调用 set
方法时,都会清除实例化选项。要为同一解析器设置附加选项,请使用 getResolver
方法检索现有解析器。
$container->set(D::class)->shared(); // Correct $container->getResolver(D::class)->alias(FooInterface::class, FooInterfaceImpl::class); // Incorrect - shared() will disappear $container->set(D::class)->alias(FooInterface::class, FooInterfaceImpl::class);
别名
考虑以下声明
interface FooInterface {} class FooInterfaceImpl implements FooInterface {} class FooInterfaceSubclass extends FooInterfaceImpl {} class D { public function __construct(FooInterface $i) {} }
D
无法通过自动装配实例化,因为 FooInterface
是一个接口。我们需要指定实现该接口的哪个具体类。我们可以通过解析器上的 alias
方法来完成此操作。
$container->set(D::class)->alias(FooInterface::class, FooInterfaceImpl::class); $d = $container->get(D::class); // This is equivalent to new D(new FooInterfaceImpl())
您可以通过单个调用设置多个别名,只需传递一个数组。
$container->set(E::class)->alias([ FooInterface::class => FooInterfaceImpl::class, BarInterface::class => BarInterfaceImpl::class ]);
别名可以引用其他别名。在下面的示例中,当容器查找 FooInterface
时,它找到 FooInterfaceImpl
作为别名,但它又引用了 FooInterfaceSubclass
。最终,创建了 FooInterfaceSubclass
。您还可以看到,别名可以用于类以及接口。
$container->set(D::class) ->alias(FooInterface::class, FooInterfaceImpl::class) ->alias(FooInterfaceImpl::class, FooInterfaceSubclass::class); $d = $container->get(D::class); // This is equivalent to new D(new FooInterfaceSubclass())
如果您想为容器中的所有类定义一个通用的别名,请考虑使用 全局别名。
构造函数参数
考虑以下一系列类
class F {} class G { public function __construct(F $f, string $host, int $port = 80) {} }
G
无法通过自动装配实例化,因为我们至少需要指定 $host
。我们可以通过解析器上的 args
方法来完成此操作。
$container->set(G::class)->args('example.com', 8080); // Multiple calls also work $container->set(G::class) ->args('example.com') ->args(8080); // Optional parameters can be omitted $container->set(G::class)->args('example.com');
仅可以使用 args
方法指定参数,这些参数
- 没有类型提示;或者
- 有内部类型(例如int、string、bool)的类型提示;或者
- 对于PHP 8,有联合类型的类型提示。
其他参数(即具有类或接口类型提示的参数)在处理args
方法时将被忽略。要操纵这些参数的处理方式,请使用别名或全局别名。
class H { // PHP 8 is required for this declaration to work public function __construct(F $f, int|string $bar, A $a, $baz) {} } // This sets $bar to 'one' and $baz to 'two' $container->set(H::class)->args('one', 'two');
有时您可能需要将容器中的内容用作参数。如果声明中的参数没有类型提示(因此您不能使用alias
),或者如果您想指定特定的命名实例,这种情况就会发生。在这些情况下,您需要使用Container::ref()
将条目标识符包装起来。
class I { /** * @param FooInterface $foo */ public function __construct($foo) {} } // See $foo to the FooInterfaceImpl from the container (which may be // instantiated if required) $container->set(I::class)->args(LightContainer\Container::ref(FooInterfaceImpl::class)); // Set $foo to named instance @foo from the container $container->set(I::class)->args(LightContainer\Container::ref('@foo'));
setter注入
除了通过构造函数进行依赖注入之外,LightContainer还支持通过setter方法进行依赖注入。考虑以下声明:
class J { public function setA(A $a) {} }
要使容器在创建J
时调用setA
来注入A
,请使用call
方法指定要调用的方法。自动装配(Autowiring)与构造函数注入的方式相同。
$container->set(J::class)->call('setA');
call
方法还接受额外的参数,这些参数将传递给setter方法。指定额外参数的规则与构造函数相同。此外,别名和全局别名的解析方式也与构造函数注入相同。
class K { public function setFoo(FooInterface $f, bool $debug); } $container->set(K::class) ->alias(FooInterface::class, FooInterfaceImpl::class) ->call('setFoo', false);
注意。别名适用于构造函数和所有setter方法。您不能定义仅适用于特定setter方法的别名。
修改已解析的实例
除了setter注入之外,LightContainer还支持在将解析对象传递回调用者之前修改该对象的其他形式。
为此,创建一个实现LightContainer\InstanceModifierInterface
的类的实例,然后使用modify
方法将其传递给容器。
class Modifier implements InstanceModifierInterface { public function modify(object $obj, LightContainerInterface $container): object {} } class ModifyMe {} $modifier = new Modifier(); $container->set(ModifyMe::class)->modify($modifier); // The container will call $modifier->modify() before returning the instance $modify_me = $container->get(ModifyMe::class);
共享实例
有时您可能希望无论容器如何解析都返回类的相同实例。如果该对象打算用作单例,则可能是这种情况。
为此,请在解析器上调用shared
方法。
$container->set(A::class)->shared(); // $a1 and $a2 are the same instance $a1 = $container->get(A::class); $a2 = $container->get(A::class);
您还可以通过调用shared(false)
来关闭此行为。但是,这只在共享实例尚未创建(即尚未调用get
)的情况下有效。否则,这将抛出异常。
您还可以使用LightContainer\Attributes\Shared
属性设置此选项。
#[Shared] class A {} $container->set(A::class); // This is equivalent to: // $container->set(A::class)->shared();
自动装配解析器的选项
自动装配解析器由容器在自动装配过程中自动创建。由于自动装配解析器在容器中没有显式条目,它们继承了容器中具有条目的直接父类的实例化选项。
例如,在下面的声明中,N
的解析器被自动装配,因为在容器中没有为N
创建显式条目。因为L
是N
的祖先类并且它在容器中有条目,所以N
继承了所有来自L
的实例化选项。
class L {} class M extends L {} class N extends M {} $container->set(L::class)->shared(); // $n1 and $n2 are the same instance because // N inherited 'shared' from L $n1 = $container->get(N::class); $n2 = $container->get(N::class);
要控制此行为,请在解析器上调用propagate(false)
。这将阻止实例化选项传播到为其子类创建的自动装配解析器。例如,在下面的示例中,N
是一个自动装配解析器,但它没有从L
继承选项,因为L
的propagate
被设置为false。因此,L
保留了不创建共享实例的默认行为。
$container->set(L::class)->shared()->propagate(false); // $n1 and $n2 are the different instances because // L does not propagate its options to autowired // subclasses $n1 = $container->get(N::class); $n2 = $container->get(N::class);
您也可以使用 LightContainer\Attributes\Propagate
属性来设置此选项。
#[Propagate(false)] class L {}
要设置所有自动装配解析器的实例化选项,您可以使用特殊通配符解析器 *
。
// Set 'shared' to true for all autowired resolvers $container->set('*')->shared();
全局别名
您可以在类级别之外定义别名,而是定义一个 全局别名,该别名适用于容器创建的所有类。这可以通过调用 set
方法实现,将需要替换的类或接口名称作为第一个参数,将具体类名称作为第二个参数。
请注意,类或接口实际上不需要存在。LightContainer 只检查第一个参数是否仅包含可以用于类型完全限定名称的字符(即包括命名空间),如果是,则将其作为全局别名处理。否则,将其视为一个 命名实例。
$container->set(FooInterface::class, FooInterfaceImpl::class);
如果类级别也定义了别名,则该定义优先于全局别名。
class FooInterfaceImplGlobal implements FooInterface {} class FooInterfaceImplForD implements FooInterface {} $container->set(FooInterface::class, FooInterfaceImplGlobal::class); $container->set(D::class)->alias(FooInterface::class, FooInterfaceImplForD::class); // D uses FooInterfaceImplForD instead of FooInterfaceImplGlobal $container->get(D::class);
与类级别定义的别名不同,您可以为全局别名设置实例化选项。这些选项应用于引用全局别名标识符而不是具体类创建的对象。以下实例化选项受支持
在全局别名中指定的实例化选项优先于为具体类定义的选项。
多个共享实例
此 shared
实例化选项将为整个容器提供一个类的单个共享实例。然而,您可能希望拥有多个跨容器共享的同一类的实例。
class O { public function __construct(\PDO $db) {} } $container->set('@prod_db', \PDO::class) ->args('mysql:host=example.com;dbname=prod'); $container->set('@dev_db', \PDO::class) ->args('mysql:host=example.com;dbname=dev'); $container->set(O::class)->alias(\PDO::class, '@prod_db');
命名实例仅是 全局别名 的特殊类型,因此它们可以在接受别名的任何地方使用。特别是,它们使用 alias
而不是 args
注入到构造函数和设置器方法中。
// Correct $container->set(O::class)->alias(\PDO::class, '@prod_db'); // Incorrect $container->set(O::class)->args('@prod_db');
此外,由于命名实例仅是全局别名的特殊类型,您可以为它们设置实例化选项(除 shared
之外)。
自定义实例化
您可以通过使用指向特定类对象的创建函数的可调用对象来指定自定义工厂函数,而不是使用 LightContainer 的默认实例化函数进行自动装配。这是通过调用 set
方法并将指向工厂函数的可调用对象作为第二个参数来实现的。
容器作为第一个参数传递给工厂函数。
$container->set(CustomClass::class, function ($container) { $a = new CustomClass(); // Custom code here return $a; });
实例化选项对自定义工厂函数不可用。由于工厂函数不支持共享,命名实例 也不可用。调用命名实例将简单地调用工厂函数,这可能会导致返回不同的对象实例。
存储任意值
您可以通过使用非字符串、非可调用的值作为第二个参数调用 set
方法在容器中存储任意值。
$container->set('@port', 8080); $container->set('@config', ['foo' => 'bar']); // Returns 8080 $container->get('@port');
建议您使用一个无法与类名称混淆的标识符(例如,通过在上面的示例中添加类名称无效字符,如 @
)。
警告。 如果您想存储 null
、字符串或可调用对象,您需要使用 Container::value()
将其包装。否则,LightContainer 会将其视为一个 全局别名 或一个 自定义实例化函数。
// Correct $container->set('@host', LightContainer\Container::value('example.com')); // Incorrect $container->set('@host', 'example.com');
配置服务
在某些情况下,使用接口定义的服务会使用具体类来实现。然后使用容器将服务与其实现链接起来。
服务可以通过以下方式识别
- 使用
LightContainer\Attributes\Service
属性;或者 - 扩展
LightContainer\ServiceInterface
。
// PHP 8 #[Service] interface FooService {} #[Service] interface BarService {} // PHP 7 interface FooService extends ServiceInterface {} interface BarService extends ServiceInterface {} class Baz implements FooService, BarService {}
您不需要为每个服务单独调用 set
方法,可以直接在具体类上调用 populate
方法。此方法使用反射来确定类实现了哪些服务,然后相应地创建容器条目。
// This: $container->populate(Baz::class); // is equivalent to: $container->set(FooService::class, Baz::class); $container->set(BarService::class, Baz::class);
加载配置
除了程序化配置容器外,您还可以使用 load
方法将预定义的配置直接加载到容器中。此方法接受容器配置为纯 PHP 数组,您可以使用任何方法(例如从 JSON 或 YAML 文件加载)来填充它。
try { $config = [ 'FooInterface' => 'FooInterfaceImpl', '@host' => [ '_value' => 'example.com' ] ]; $container->load($config); } catch (LightContainer\Loader\LoaderException $e) { }
有关配置数组应如何结构的详细信息,请参阅 配置格式 文档。注意,此格式仅支持 LightContainer 的部分功能。
参考
解析器
在核心上,LightContainer 存储了一个条目标识符与解析器之间的映射。解析器是实现了 LightContainer\Resolvers\ResolverInterface
的对象,容器在每次调用 get
方法时都会使用它来解析对象或其它值。
主要的解析器类型是 类解析器,它通过从指定的类实例化来解析对象。其他主要类型的解析器包括 引用解析器,它查找容器中的另一个条目,以及 值解析器,它简单地返回指定的值。
set
方法接受字符串条目标识符和指定的值,创建并注册解析器,然后将它返回给用户。该方法创建的解析器类型取决于条目标识符的格式和指定的值的类型。以下表格中列出了这些信息。
返回的 *
类解析器是一种特殊的类解析器。它不能被调用以解析到实际对象。
自动装配
自动注入通过使用 PHP 反射检查构造函数(或设置方法)的参数来实现。如果任何参数有类型提示,并且提示的类型不是内置字面量类型,则它会在容器中查找是否有具有该类型作为条目标识符的现有条目。如果不存在条目,但指定的类型是类且类存在,则容器会自动创建一个针对该类型的类解析器(自动注入解析器)。此自动注入解析器的实例化选项基于上述描述的 传播规则。
请注意,当向构造函数或设置方法注入参数时,可空类型标识符(?
)和默认值声明最初会被忽略。容器将尝试创建和注入特定类型的实例,即使它被声明为可空的或包含默认值。只有当容器无法创建实例时,才会使用 null 或默认值。
interface ImplementedInterface {} class InterfaceImplementation implements ImplementedInterface {} interface UnimplementedInterface {} class P1 { function __construct(?ImplementedInterface $i) {} } class P2 { function __construct(ImplementedInterface $i = null) {} } class Q1 { function __construct(?UnimplementedInterface $i) {} } class Q2 { function __construct(UnimplementedInterface $i = null) {} } $container->set(ImplementedInterface::class, InterfaceImplementation::class); // $p1->i and $p2->i will both contain a InterfaceImplementation, // as an alias is registered for ImplementedInterface and the aliased class // exists $p1 = $container->get(P1::class); $p2 = $container->get(P2::class); // $q1->i and $q2->i will both be null, as there is no alias registered for // UnimplementedInterface $q1 = $container->get(Q1::class); $q2 = $container->get(Q2::class);
为了将 null 作为可空类型的参数传递,您必须使用 alias
显式声明一个指向 null 的别名。
$container->set(P1::class)->alias(ImplementedInterface::class, null); $container->set(P2::class)->alias(ImplementedInterface::class, null); // $p1->i and $p2->i are now both null $p1 = $container->get(P1::class); $p2 = $container->get(P2::class);
实例化选项参考
类解析器有以下实例化选项可用。这些选项(除 propagate
之外)也适用于 全局别名。
许可证
BSD 3条款