在构建复杂的PHP应用程序时,我们可以依靠依赖性注入和服务容器来管理应用程序中的对象或 "服务 "的实例化。
有几个依赖注入库满足PSR-11,即描述 "容器接口 "契约的PHP标准建议。
Symfony的DependencyInjection在GitHub上有3.4K颗星,比同类库要高一个档次。它非常强大,但使用起来却很简单。由于所有服务必须初始化的逻辑可以生成并转储为一个PHP文件,所以它在生产中运行得很快。它可以被配置成同时为PHP和YAML提供服务。而且它很容易理解,因为它有大量的文档支持。
使用服务容器已经有助于管理复杂的应用程序。同样重要的是,服务容器减少了对外部开发人员为我们的应用程序编写代码的需求。
例如,我们的PHP应用程序可以通过模块进行扩展,而第三方开发者可以编写他们自己的扩展。通过使用服务容器,我们使他们更容易将他们的服务注入到我们的应用中,即使他们对我们的应用如何工作没有深刻的理解。这是因为我们可以通过编程规则来定义服务容器如何初始化服务,并使这个过程自动化。
这种自动化转化为开发人员不必再做的工作。因此,他们不需要了解服务如何初始化的内部的、琐碎的细节;那是由服务容器来处理的。
尽管开发者仍然需要理解依赖注入和容器服务背后的概念,但通过使用DependencyInjection库,我们可以简单地将他们引向Symfony关于这个主题的文档。减少我们需要维护的文档数量会让我们更高兴,并腾出时间和资源来处理我们的代码。
在这篇文章中,我们将看一些如何使用DependencyInjection库来使PHP应用更具有扩展性的例子。
使用编译器传递的工作
编译器传递是库的机制,用于修改容器中的服务如何在服务容器被编译之前被初始化和调用。
一个编译器传递对象必须实现 CompilerPassInterface:
>use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OurCustomPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
// ... do something during the compilation
}
}
为了在我们的应用程序中注册它,我们要做以下工作。
use Symfony\Component\DependencyInjection\ContainerBuilder;
$containerBuilder = new ContainerBuilder();
$containerBuilder->addCompilerPass(new OurCustomPass());
// Inject all the compiler passes
foreach ($compilerPasses as $compilerPass) {
$containerBuilder->addCompilerPass($compilerPass);
}
// Compile the container
$containerBuilder->compile();
自动初始化服务
通过编译器传递,我们可以自动初始化某种类型的服务--例如,任何从某个类延伸出来的类,实现了某些接口,有某个服务标签分配给它的定义,或其他一些自定义行为。
让我们看一个例子。我们将使我们的PHP应用程序自动初始化任何实现了以下功能的对象 AutomaticallyInstantiatedServiceInterface的对象,调用它的initialize 方法。
interface AutomaticallyInstantiatedServiceInterface
{
public function initialize(): void;
}
然后我们可以创建一个编译器传递,它将遍历容器中定义的所有服务的列表,并识别那些实现AutomaticallyInstantiatedServiceInterface 。
class AutomaticallyInstantiateServiceCustomPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definitions = $container->getDefinitions();
foreach ($definitions as $definitionID => $definition) {
$definitionClass = $definition->getClass();
if ($definitionClass === null || !is_a($definitionClass, AutomaticallyInstantiatedServiceInterface::class, true)) {
continue;
}
// $definition is a AutomaticallyInstantiatedServiceInterface
// Do something with it
// ...
}
}
}
接下来,我们将创建一个名为ServiceInstantiatorInterface 的服务,它将负责初始化所识别的服务。通过addService 方法,它将收集所有要初始化的服务,其方法initializeServices 将最终被 PHP 应用程序调用。
interface ServiceInstantiatorInterface
{
public function addService(AutomaticallyInstantiatedServiceInterface $service): void;
public function initializeServices(): void;
}
这个服务的实现可以在GitHub上找到。
class ServiceInstantiator implements ServiceInstantiatorInterface
{
/**
* @var AutomaticallyInstantiatedServiceInterface[]
*/
protected array $services = [];
public function addService(AutomaticallyInstantiatedServiceInterface $service): void
{
$this->services[] = $service;
}
public function initializeServices(): void
{
foreach ($this->services as $service) {
$service->initialize();
}
}
}
现在我们可以通过将所有确定的服务注入到ServiceInstantiatorInterface 服务中来完成上述编译器传递的代码。
class AutomaticallyInstantiateServiceCustomPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$serviceInstantiatorDefinition = $container->getDefinition(ServiceInstantiatorInterface::class);
$definitions = $container->getDefinitions();
foreach ($definitions as $definitionID => $definition) {
$definitionClass = $definition->getClass();
if ($definitionClass === null) {
continue;
}
if (!is_a($definitionClass, AutomaticallyInstantiatedServiceInterface::class, true)) {
continue;
}
// $definition is a AutomaticallyInstantiatedServiceInterface
// Do something with it
$serviceInstantiatorDefinition->addMethodCall(
'addService',
[new Reference($definitionID)]
);
}
}
}
作为一个服务本身,ServiceInstantiatorInterface 的定义也可以在服务容器上找到。这就是为什么,为了获得这个服务的引用,我们必须做。
$serviceInstantiatorDefinition = $container->getDefinition(ServiceInstantiatorInterface::class);
我们不是在处理实例化的对象/服务,因为我们还没有它们。相反,我们要处理的是容器上的服务定义。这也是为什么,要把一个服务注入另一个服务,我们不能这样做。
$serviceInstantiator->addService(new $definitionClass());
而是必须这样做。
$serviceInstantiatorDefinition->addMethodCall(
'addService',
[new Reference($definitionID)]
);
PHP应用程序必须在启动时触发服务的初始化。
$serviceInstantiator->initializeServices();
最后,我们实现那些需要自动初始化的服务。
AutomaticallyInstantiatedServiceInterface
在这个例子中,我们的应用程序使用SchemaConfiguratorExecuter 服务。初始化逻辑已经被它们的祖先类所满足。 [AbstractSchemaConfiguratorExecuter](https://github.com/leoloso/PoP/blob/ad33f7e1bcb83309718272008b11079582b3a718/layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/src/Services/SchemaConfiguratorExecuters/AbstractSchemaConfiguratorExecuter.php),像这样。
abstract class AbstractSchemaConfiguratorExecuter implements AutomaticallyInstantiatedServiceInterface
{
public function initialize(): void
{
if ($customPostID = $this->getCustomPostID()) {
$schemaConfigurator = $this->getSchemaConfigurator();
$schemaConfigurator->executeSchemaConfiguration($customPostID);
}
}
/**
* Provide the ID of the custom post containing the Schema Configuration block
*/
abstract protected function getCustomPostID(): ?int;
/**
* Initialize the configuration of services before the execution of the GraphQL query
*/
abstract protected function getSchemaConfigurator(): SchemaConfiguratorInterface;
}
现在,任何想创建自己的SchemaConfiguratorExecuter 服务的第三方开发者只需要创建一个继承自AbstractSchemaConfiguratorExecuter 的类,满足抽象方法,并在其服务容器配置中定义该类。
然后,服务容器将根据应用程序生命周期的要求,负责实例化和初始化该类。
注册但不初始化服务
在某些情况下,我们可能想禁用一个服务。在我们的PHP应用程序的例子中,WordPress的GraphQL服务器允许用户从GraphQL模式中删除类型。如果网站上的博客文章不显示评论,那么我们可以跳过将Comment 类型添加到模式中。
[CommentTypeResolver](https://github.com/leoloso/PoP/blob/master/layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/vendor/pop-schema/comments/src/TypeResolvers/CommentTypeResolver.php)是将Comment 类型添加到模式中的服务。为了跳过将这个类型添加到模式中,我们所要做的就是不在容器中注册这个服务。
但是这样做,我们就会遇到一个问题:如果任何其他的服务中注入了CommentTypeResolver (比如这个),那么这个实例化就会失败,因为DependencyInjection不知道如何解决这个服务,会抛出一个错误。
Fatal error: Uncaught Symfony\Component\DependencyInjection\Exception\RuntimeException: Cannot autowire service "GraphQLAPI\GraphQLAPI\ModuleResolvers\SchemaTypeModuleResolver": argument "$commentTypeResolver" of method "__construct()" references class "PoPSchema\Comments\TypeResolvers\CommentTypeResolver" but no such service exists. in /app/wordpress/wp-content/plugins/graphql-api/vendor/symfony/dependency-injection/Compiler/DefinitionErrorExceptionPass.php:54
这意味着CommentTypeResolver 和所有其他服务必须始终在容器服务中注册--也就是说,除非我们绝对确定它不会被其他服务所引用。正如下面所解释的,在我们的示例应用程序中,有些服务只在管理端可用,所以我们可以跳过为面向用户的一方注册它们。
从模式中删除Comment 类型的解决方案必须是实例化服务,这应该是没有副作用的,但不是初始化它,因为在那里确实会发生副作用。
为了实现这一点,我们可以在注册服务时使用autoconfigure属性来表示必须初始化服务。
services:
PoPSchema\Comments\TypeResolvers\CommentTypeResolver:
class: ~
autoconfigure: true
而且我们可以更新编译器的传递,只将那些带有autoconfigure: true 的服务注入到ServiceInstantiatorInterface 。
class AutomaticallyInstantiateServiceCustomPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
// ...
foreach ($definitions as $definitionID => $definition) {
// ...
if ($definition->isAutoconfigured()) {
// $definition is a AutomaticallyInstantiatedServiceInterface
// Do something with it
$serviceInstantiatorDefinition->addMethodCall(
'addService',
[new Reference($definitionID)]
);
}
}
}
}
表明有条件的服务初始化
上面的解决方案是可行的,但它有一个很大的问题:定义服务是否必须被初始化必须在服务定义文件上设置,该文件在容器编译时被访问--也就是说,在我们可以在应用程序中开始使用服务之前。在某些情况下,我们可能还想根据运行时的值来禁用服务,比如当管理员用户通过应用程序设置禁用Comment ,而这些设置会保存在数据库中。
为了解决这个问题,我们可以让服务本身指示它是否必须被初始化。为此,我们将isServiceEnabled 方法添加到其接口中。
interface AutomaticallyInstantiatedServiceInterface
{
// ...
public function isServiceEnabled(): bool;
}
例如,在我们的例子中,PHP应用程序的一个服务是这样实现这个方法的。
abstract class AbstractScript implements AutomaticallyInstantiatedServiceInterface
{
/**
* Only enable the service, if the corresponding module is also enabled
*/
public function isServiceEnabled(): bool
{
$enablingModule = $this->getEnablingModule();
return $this->moduleRegistry->isModuleEnabled($enablingModule);
}
}
最后,ServiceInstantiatorInterface 服务可以识别那些必须被初始化的服务。
class ServiceInstantiator implements ServiceInstantiatorInterface
{
// ...
public function initializeServices(): void
{
$enabledServices = array_filter(
$this->services,
fn ($service) => $service->isServiceEnabled()
);
foreach ($enabledServices as $service) {
$service->initialize();
}
}
}
这样,我们不仅能够在配置服务容器时跳过初始化一个服务,而且在运行应用程序时也能动态地跳过初始化。
为不同的行为注册不同的容器服务
PHP应用程序并不局限于只有一个服务容器。例如,应用程序可以根据给定的条件表现出不同的行为,如在管理员端或面向用户端。这意味着,根据不同的环境,应用程序需要注册不同的服务集。
为了实现这一点,我们可以将services.yaml 配置文件分成几个子文件,并在需要的时候注册每一个子文件。
services.yaml的这个定义应该总是被加载,因为它将注册所有在Services/ 下发现的服务。
services:
_defaults:
public: true
autowire: true
GraphQLAPI\GraphQLAPI\Services\:
resource: 'src/Services/*'
而另一个关于Conditional/Admin/services.yaml的定义是有条件的,只在管理员端加载,注册所有在Conditional/Admin/Services/ 下发现的服务。
services:
_defaults:
public: true
autowire: true
GraphQLAPI\GraphQLAPI\Conditional\Admin\Services\:
resource: 'src/Conditional/Admin/Services/*'
下面的代码总是注册第一个文件,但只有在管理端时才注册第二个文件。
self::initServices('services.yaml');
if (is_admin()) {
self::initServices('Conditional/Admin/services.yaml');
}
现在我们必须记住,对于生产来说,DependencyInjection将把编译后的服务容器转储到一个PHP文件中。我们还需要产生两个不同的转储文件,并为每个上下文加载相应的文件。
public function getCachedContainerFileName(): string
{
$fileName = 'container_cache';
if (is_admin()) {
$fileName .= '_admin';
}
return $fileName . '.php';
}
在配置之上建立惯例
惯例重于配置是为项目建立规范的艺术,以应用一个标准的行为,不仅可以工作,而且可以减少开发人员所需的配置量。
这一策略的实施可能需要我们将某些文件放在某些文件夹中。例如,为了实例化某些框架的EventListener 对象,我们可能需要将所有相应的文件放在一个EventListeners 文件夹下,或者将其分配给app\EventListeners 命名空间。
请注意,编译器通过可以消除这样的要求。为了识别一个服务并以特殊的方式对待它,该服务必须扩展一些类,实现一些接口,被分配一些服务标签,或者显示一些其他的自定义行为--与它的位置无关。
由于编译器的传递,我们的PHP应用可以自然地为创建扩展的开发者提供约定俗成的配置,同时减少其不便之处。
通过文件夹结构暴露服务的信息
即使我们不需要将文件放在任何特定的文件夹中,但如果它能达到一些除初始化服务之外的目的,我们仍然可以为应用程序设计一个逻辑结构。
在我们的PHP应用程序的例子中,让我们让文件夹结构传达哪些服务是可用的,它们是否必须在容器中隐式定义,以及在什么情况下它们将被添加到容器中。
为此,我们将使用以下结构。
- 所有访问特定服务的界面都在下面
Facades/ - 所有总是被初始化的服务都在下面
Services/ - 所有有条件的服务,根据上下文可能被初始化也可能不被初始化,放在下面
Conditional/{ConditionName}/Services - 所有覆盖默认实现的服务的实现,由一些包提供,放在下面
Overrides/Services - 所有通过其契约而不是直接作为实现被访问的服务,如服务
ServiceInstantiatorInterface,可以放在任何地方,因为它们在容器中的定义必须是明确的。
services:
_defaults:
public: true
autowire: truePoP\Root\Container\ServiceInstantiatorInterface:
class: \PoP\Root\Container\ServiceInstantiator
我们使用什么结构完全取决于我们自己,基于我们应用程序的需要。
结论
为PHP应用程序创建一个强大的架构,即使只是为我们自己的开发团队,也已经是一个挑战。对于这些情况,使用依赖注入和容器服务可以大大简化任务。
在此基础上,如果我们还需要允许第三方--他们可能并不完全了解应用程序的工作原理--提供扩展,那么挑战就更大了。当使用DependencyInjection组件时,我们可以创建编译器通道来自动配置和初始化应用程序,从而消除开发人员的这种需要。
The postBuilding extensible PHP apps with Symfony DIappeared first onLogRocket Blog.