inpsyde/wp-app-container

在网站级别使用的DI容器和相关工具。

3.0.0-beta.3 2024-03-01 18:53 UTC

README

在网站级别使用的DI容器和相关工具.

PHP Quality Assurance

目录

什么是以及什么不是

这是一个针对应用(即网站)级别的依赖注入容器、服务提供商和应用程序“引导”的包。

典型的用例是当为客户构建网站时,我们预计要编写几个“包”:库、插件和主题,然后使用Composer将它们“粘合”在一起。

得益于这个包,可以实现集中的依赖解析,并为这些包的后端提供相当标准化和一致的结构。

从技术上来说,目前没有任何东西阻止在包级别使用,然而由于几个原因,这不是这个包的目标,并且不会添加任何代码来符合这一点。

这个包不是被写成“仅仅是一个标准”,即只提供抽象,将实现留给消费者,而是被写成现成的实现。

然而,对于PSR-11的基本支持允许非常灵活的使用。

概念概述

应用程序

这是包的核心类。这是“应用程序引导”发生的地方,服务提供商在这里注册,并且很可能只需要从网站“包”(即通过Composer“粘合”其他包/插件/主题的那个包)使用这个对象。

服务提供商

该包提供了一个单一的服务提供商接口(以及几个部分实现它的抽象类)。这些对象用于“组合”容器。此外,在这个包的实现中,服务提供商(或者更好的,可以负责)告诉如何使用注册的服务。在WordPress世界中,这很可能意味着“添加钩子”。

容器

这是一个“存储”系统,能够通过唯一标识符存储和检索对象。在检索时(通常是第一次检索),对象会被“解析”,这意味着构建目标对象所需的任何其他对象,将首先在容器中进行递归解析,然后再将其注入到目标对象中再返回。这里提供的容器实现是Pimple的扩展,增加了PSR-11支持,并且可以充当多个其他PSR-11容器的“代理”。这意味着服务提供者可以通过直接将服务工厂添加到底层Pimple容器中,或者将现成的PSR-11容器“附加”到主容器中。

环境配置

如上所述,此包针对网站开发,在网站级别需要的是配置。与WordPress配置一起工作通常意味着PHP常量,但当在网站级别使用Composer时,例如与WP Starter结合使用,这也意味着环境变量。该包提供了一个SiteConfig接口和一个EnvConfig实现,它在此方面不涉及存储配置,但提供了一个非常灵活的方式从常量和env变量中读取配置。Container::config()方法返回一个SiteConfig实例。

上下文

服务提供者的工作是向容器中添加服务并添加使用它们的钩子,然而在WordPress中,服务通常在特定的“上下文”下需要。例如,负责注册和排入前端资源的服务提供者不在后台(仪表板)中需要,也不在AJAX或REST请求中需要,等等。使用适当的钩子来执行代码是可以经常解决的,但往往不是。例如,在早期钩子中区分REST请求并不容易,或者没有函数或常量告诉我们我们是否在登录页面,等等。此外,即使将对象工厂存储在容器中,我们确定不会使用它们,也是可以避免的内存浪费。此包的Context类是一个提供当前请求信息的集中式服务。Container::context()方法返回一个Context实例。

决策

因为我们想要一个现成的包,我们需要选择一个DI容器实现,我们选择了Pimple,因为它是非常简单实现之一。

然而,正如后面所显示的,任何想使用不同的PSR-11容器的人都将能够这样做。

在上面的“概念概述”中,最后两个概念(“Env Config”和“Context”)实际上并不是与其他三个自然结合的,然而,这个假设是,这个包将被用于WordPress 网站,这允许我们无太大风险(或罪恶感)地引入这种“结合”:假设我们将这些包分开提供,当构建网站(这是此包的唯一目标)时,我们仍然很可能需要那些单独的包,这使得这成为一个完美的共同重用原则的例子:倾向于一起重用的类应放在同一个包中。

在网站级别使用

将所有包粘合在一起的“网站”包只需以非常简单的方式与App类交互

<?php
namespace AcmeInc;

add_action('muplugins_loaded', [\Inpsyde\App\App::new(), 'boot']);

就这么多。假设此代码放置在MU插件中,但正如后面更好地解释的,可以在任何MU插件或插件外进行,要么将App::boot()调用包装在钩子中,要么不包装。

这行代码将创建相关对象并触发动作,使其他包能够注册服务提供者。

自定义网站配置

通过使用App::new()创建App的实例,它会负责创建一个EnvConfig的实例,该实例将在调用Container::config()时返回。EnvConfig是一个对象,允许检索有关当前环境的信息(例如生产测试开发...),还可以获取存储为PHP常量或环境变量的设置。

有关运行环境的信息自动从由WP Starter支持的env变量或从知名托管服务如Automattic VIP或WP Engine定义的配置中自动发现。如果无法确定环境,将有一个回退:如果WP_DEBUG为true,则假定是开发环境,否则是生产环境。

在任何情况下,都有一个名为"wp-app-environment"的过滤器,可用于自定义确定的 环境。

关于PHP常量,EnvConfig能够搜索根命名空间中定义的常量,也可以在其它命名空间中搜索。在后一种情况下,类需要配置以让它知道哪些“替代”命名空间被支持。

这可以通过创建一个使用自定义EnvConfig实例的Container实例来完成,然后将它传递给App::new()。例如

<?php
namespace AcmeInc;

use Inpsyde\App;

$container = new App\Container(new App\EnvConfig('AcmeInc\Config', 'AcmeInc'));
App\App::new($container)->boot();

在上面的代码片段中,创建的EnvConfig实例(将通过Container::config()方法可用)可以返回在AcmeInc\ConfigAcmeInc命名空间中的设置(除了根命名空间)。

例如,如果某个配置文件包含

<?php
define('AcmeInc\Config\ONE', 1);
define('AcmeInc\TWO', 2);

则可以进行以下操作

<?php
/** @var Inpsyde\App\Container $container */
$container->config()->get('ONE'); // 1
$container->config()->get('TWO'); // 2

注意,EnvConfig::get()接受一个可选的第二个参数$default,如果没有为给定名称设置常量或匹配的环境变量,则返回该参数。

<?php
/** @var Inpsyde\App\Container $container */
$container->config()->get('SOMETHING_NOT_DEFINED', 3); // 3

托管提供商

EnvConfig::hosting()返回当前的托管服务提供商。目前,我们自动检测以下

  • EnvConfig::HOSTING_VIP - WordPress VIP Go
  • EnvConfig::HOSTING_WPE - WP Engine
  • EnvConfig::HOSTING_SPACES - Mittwald Spaces
  • EnvConfig::HOSTING_OTHER - 如果未检测到上述任何一项

可以通过一个名为HOSTING的env变量或常量来设置自定义的托管服务。

要检查代码中当前解决方案,有一个名为EnvConfig::hostingIs()的方法,它接受一个托管服务名称字符串,并在给定的托管服务与当前托管服务匹配时返回true。

位置

访问位置

EnvConfig::locations()返回一个Inpsyde\App\Location\Locations的实例,该实例允许解析以下目录和URL

  • mu-plugins
  • plugins
  • themes
  • languages
  • vendor

在VIP Go(HOSTING值将为EnvConfig::HOSTING_VIP)上,可以获得额外的位置

  • private
  • config
  • vip-config
  • images

实际上,Locations是一个接口,目前有三个实现,一个用于“通用”托管,一个用于VIP Go,一个用于WP Engine。

一个例子

/** @var Inpsyde\App\EnvConfig $envConfig */
$location = $envConfig->locations();

$vendorPath = $location->vendorDir();                   // vendor directory path
$wonologPath = $location->vendorDir('inpsyde/wonolog'); // specific package path

$pluginsUrl = $location->pluginsUrl();                   // plugins directory URL
$yoastSeoUrl = $location->pluginsUrl('/wordpress-seo/'); // specific plugin URL

调整位置

如果包无法自动发现路径和URL(例如,因为一个非常定制的设置),可以使用一个LOCATIONS常量来设置它们,它是一个包含两个顶级元素的数组,一个用于URL,一个用于路径,每个元素都是一个形式为数组的映射,其中位置名称作为键,位置URL/路径作为值

例如

namespace AwesomeWebsite\Config;
 
use Inpsyde\App\Location\Locations;
use Inpsyde\App\Location\LocationResolver;

const LOCATIONS = [
    LocationResolver::URL => [
        Locations::VENDOR => 'http://example.com/wp/wp-content/composer/vendor/',
        Locations::ROOT => __DIR__,
        Locations::CONTENT => 'http://content.example.com/',
    ],
    LocationResolver::DIR => [
        Locations::VENDOR => '/var/www/wp/wp-content/composer/vendor/',
        Locations::ROOT => dirname(__DIR__),
        Locations::CONTENT => '/var/www/content/',
    ],
];

除了Locations::VENDORLocations::ROOTLocations::CONTENT之外,还可以使用任何其他Locations常量,例如Locations::MU_PLUGINSLocations::LANGUAGES等。

提供的配置将与默认配置合并,可以根据托管服务进行微调。

自定义位置

除了Locations常量之外,还可以使用自定义键,并使用Locations::resolveDir()Locations::resolveUrl()方法检索它们。

例如

namespace AwesomeWebsite\Config;
 
use Inpsyde\App\Location\LocationResolver;

const LOCATIONS = [
    LocationResolver::DIR => [
        'logs' => '/var/www/logs/',
    ],
];

然后

/** @var Inpsyde\App\EnvConfig $envConfig */
/** @var Inpsyde\App\Location\Locations $locations */
$locations = $envConfig->locations();

echo $locations->resolveDir('logs', '2019/10/08.log');

"/var/www/logs/2019/10/08.log"

在上面的例子中,调用$locations->resolveUrl('logs')将返回null,因为在LOCATIONS常量中未为键'logs'设置URL。

通过环境变量设置位置

在上面的示例中,默认和自定义位置都是通过使用LOCATIONS常量进行定制的,出于明显的原因,该常量只能在PHP配置文件中设置。

对于依赖于环境变量设置配置的网站,该包提供了一种不同的方法。

可以使用格式为WP_APP_{$location}_DIRWP_APP_{$location}_URL的环境变量来设置位置目录和URL。

例如,可以通过WP_APP_VENDOR_DIR设置供应商路径,通过WP_APP_VENDOR_URL设置供应商URL,就像可以通过WP_APP_ROOT_DIR设置根路径,通过WP_APP_ROOT_URL设置根URL一样。

这也适用于自定义路径。

例如,通过设置如下环境变量

WP_APP_VENDOR_DIR="/var/www/shared/vendor/"
WP_APP_LOGS_DIR="/var/www/logs/"

然后可以像这样检索它们

/** @var Inpsyde\App\EnvConfig $envConfig */
/** @var Inpsyde\App\Location\Locations $locations */
$locations = $envConfig->locations();

echo $locations->vendorDir('inpsyde/wp-app-container');
"/var/www/shared/vendor/inpsyde/wp-app-container"


echo $locations->resolveDir('logs', '2019/10');
"/var/www/logs/2019/10"

请注意,如果同时设置了同一位置的WP_APP_*环境变量和LOCATIONS常量中的值,则环境变量具有优先权。

在包级别使用

在包级别上,有两种方式可以注册服务(稍后将展示),但首先需要将提供者添加到App中

<?php
namespace AcmeInc\Foo;

use Inpsyde\App\App;
use Inpsyde\WpContext;

add_action(
    App::ACTION_ADD_PROVIDERS,
    function (App $app) {
        $app
            ->addProvider(new MainProvider(), WpContext::CORE)
            ->addProvider(new CronRestProvider(), WpContext::CRON, WpContext::REST)
            ->addProvider(new AdminProvider(), WpContext::BACKOFFICE);
    }
);

钩子App::ACTION_ADD_PROVIDERS实际上可以多次触发(稍后将详细介绍),但就目前而言,重要的是即使钩子被触发多次,App类也足够聪明,只添加提供者一次。

上下文注册

如上例所示,除了服务提供者本身外,App::addProvider()还接受可变数量的"Context"常量,这些常量告诉App给定的提供者应该仅在列出的上下文中使用。

可能的常量完整列表是

  • CORE,这基本上意味着“始终”,或者至少是“如果WordPress已加载”
  • FRONTOFFICE
  • BACKOFFICE("admin"请求,不包括AJAX请求)
  • AJAX
  • REST
  • CRON
  • LOGIN
  • CLI(在WP CLI的上下文中)

包依赖注册

除了App::ACTION_ADD_PROVIDERS之外,包还可以使用另一个钩子将服务提供者添加到App中。它是:App::ACTION_REGISTERED_PROVIDER

此钩子在注册任何提供者后立即触发。使用此钩子,只有在给定包注册的情况下才注册提供者,这样可以分发库/插件,如果其他库/插件不可用,它们可能不会做任何事情。

<?php
namespace AcmeInc\Foo\Extension;

use Inpsyde\App\App;
use Inpsyde\WpContext;
use AcmeInc\Foo\MainProvider;

add_action(
    App::ACTION_REGISTERED_PROVIDER,
    function (string $providerId, App $app) {
        if ($providerId === MainProvider::class) {
            $app->addProvider(new ExtensionProvider(), WpContext::CORE);
        }
    },
    10,
    2
);

钩子通过第一个参数传递刚注册的包ID。默认情况下,包ID是提供者类的FQCN,但这可以很容易地更改,因此要依赖包,就必须知道它使用的ID。

需要注意的是,只有当目标服务提供者的register()方法返回true时,App::ACTION_REGISTERED_PROVIDER钩子才会触发。例如,如果提供者是“仅启动”提供者(下面将详细介绍),则不会触发钩子。

在这种情况下,可以使用App::ACTION_ADDED_PROVIDER钩子,它的工作方式类似,它在提供者被添加时触发,因此在尝试注册之前。

提供者工作流程

如前所述多次提到,库的作用是为组成网站的各个包的服务注册和引导提供共同的基础。

这意味着需要允许通用库、MU插件、插件和主题注册它们的服务,这意味着理论上,应用程序应该“等待”所有这些包都可用。然而,同时,某些包可能在WordPress加载工作流程的早期阶段运行。

为了满足这两个要求,App类根据第一次调用App::boot()时的时间从一到三次运行其“引导过程”。

如果第一次调用App::boot()是在plugins_loaded钩子之前,它将自动在plugins_loaded时再次调用,然后在init时再次调用。总共调用3次。

如果第一次调用App::boot()是在plugins_loaded之后(或期间),但在init之前,它将在init时再次自动调用。总共调用2次。

如果第一次调用App::boot()是在init期间,它将不会再次调用,因此总共只会运行一次。

如果第一次调用App::boot()是在init之后,将抛出异常。

每次调用App::boot()时,都会触发App::ACTION_ADD_PROVIDERS操作,允许包添加服务提供者。

添加的服务提供者的register()方法,用于在容器中添加服务,通常立即调用,除非刚添加的服务提供者声明支持“延迟注册”(关于这一点稍后介绍)。

添加的服务提供者的boot()方法,用于使用已注册的服务,通常延迟到App::boot()最后调用时(WP在init钩子处),但服务提供者可以声明支持“早期启动”(关于这一点稍后介绍),在这种情况下,它们的boot()方法在register方法之后调用,而不必等待在init时最后调用boot

如果一个服务提供者同时支持延迟注册早期启动,它的register()方法仍然会在其boot()方法之前调用,但之后已经调用了即将在同一boot()周期中启动的所有非延迟提供者的register()方法。

考虑App::boot()运行3次的情况(在plugins_loaded之前、在plugins_loaded上、在init上),事件顺序如下

  • 核心在plugins_loaded之前

    1. 添加了不支持延迟注册的服务提供者被注册
    2. 添加了既支持延迟注册又支持早期启动的服务提供者被注册
    3. 添加了支持早期启动的服务提供者被启动
  • 核心在plugins_loaded

    1. 添加了不支持延迟注册的服务提供者被注册
    2. 添加了既支持延迟注册又支持早期启动的服务提供者被注册
    3. 添加了支持早期启动的服务提供者被启动
  • 核心在init

    1. 尚未注册的所有不支持延迟注册的添加服务提供者都被注册
    2. 尚未注册的所有支持延迟注册的添加服务提供者都被注册
    3. 尚未启动的所有添加服务提供者都被启动

要了解提供者是否支持延迟注册早期启动,我们必须查看ServiceProvider接口的两个方法,分别是registerLater()bootEarly(),两者都返回布尔值。

ServiceProvider接口总共有5个方法。除了前面提到的两个方法外,还有id()方法,以及两个最相关的:register()boot()

该包提供了一些抽象类,为其中一些方法提供了定义。所有这些类都有一个id()方法,默认返回类的名称(关于这一点稍后介绍),并定义了不同的registerLater()bootEarly()组合。其中一些还注册了空的boot()register(),为需要仅注册服务或仅启动它们的提供者。

可用的服务提供商抽象类

  • Provider\Booted是一个需要实现register()boot()方法的提供者。它不支持延迟注册,也不支持早期启动
  • Provider\BootedOnly 是一个只要求实现 boot() 方法的提供者(register() 方法已实现但无具体内容)。它不支持 早期启动
  • Provider\EarlyBooted 是一个要求同时实现 register()boot() 方法的提供者。它不支持 延迟注册,但支持 早期启动
  • Provider\EarlyBootedOnly 是一个只要求实现 boot() 方法的提供者(register() 方法已实现但无具体内容)。它支持 早期启动
  • Provider\RegisteredLater 是一个要求同时实现 register()boot() 方法的提供者。它支持 延迟注册,但不支持 早期启动
  • Provider\RegisteredLaterEarlyBooted 是一个要求同时实现 register()boot() 方法的提供者。它既支持 延迟注册,也支持 早期启动
  • Provider\RegisteredLaterOnly 是一个只要求实现 register() 方法的提供者(boot() 方法已实现但无具体内容)。它支持 延迟注册
  • Provider\RegisteredOnly 是一个只要求实现 register() 方法的提供者(boot() 方法已实现但无具体内容)。它不支持 延迟注册

通过扩展这些类中的一个,消费者可以只关注重要的方法。

延迟注册的理由

如果“正常”启动与“早期”启动提供者的原因已经提及(某些提供者需要尽早运行,但某些其他提供者不会尽早可用),这并不适用于提供者可以支持的“延迟注册”。

为了解释为什么这是必要的,让我们举一个例子。

假设一个 Acme Advanced Logger 插件提供了一个服务提供者,该提供者注册了一个 Acme\Logger 服务。

然后,假设另一个独立的插件 Acme Authentication 提供了一个服务提供者,该提供者注册了几个需要 Acme\Logger 服务的其他服务。

Acme Authentication 服务提供者需要确保 Acme\Logger 服务可用。一种常见的策略是 检查容器以确定其可用性,如果不存在(例如,Acme Advanced Logger 插件被禁用),则 Acme Authentication 注册一个可以替代缺失服务的替代日志记录器。

为了使这种可用性检查有效,必须在 Acme Advanced Logger 服务提供者注册之后进行。通过支持延迟注册,Acme Authentication 服务提供者将确保在 Acme Advanced Logger 最终注册后(假设它本身也没有延迟)注册,因此其 register 方法可以可靠地检查 Acme\Logger 服务是否已经可用。

服务提供商ID

ServiceProvider 接口的 id() 方法返回一个在多个地方使用的标识符。

例如,如上文中“依赖包注册”部分所示,它作为参数传递给 App::ACTION_REGISTERED_PROVIDER,以便包可以依赖其他包。

服务提供者 ID 也可以传递给 Container::hasProvider() 方法,以确定给定的提供者是否已注册。

该软件包提供的所有抽象服务提供者类都使用一个特质,按照顺序

  • 检查类中是否存在公共属性 $id,如果存在则使用它。
  • 如果没有 $id 公共属性,则检查类中是否存在公共常量 ID,如果存在则使用它。
  • 如果上述都不适用,则使用类的完全限定名称作为 ID。

因此,通过扩展其中一个抽象类而无需做其他任何事情,就已经定义了一个 ID,即类名。

如果出于某些原因(例如,使用同一服务提供商类为多个提供商提供服务)这不行,则可以定义属性,或直接覆盖id()方法。

注意:提供者ID必须是唯一的。尝试添加一个已使用的ID的提供者将只会跳过添加,而不会做其他任何事情。

组合容器

ServiceProvider::register()是提供者向容器添加服务的地方,这样它们就可以在ServiceProvider::boot()方法中“消费”。

ServiceProvider::register()签名如下

public function register(Container $container): void;

接收Container服务实例的提供者可以通过两种方式向其中添加内容

  • 直接使用Pimple \ArrayAccess方法
  • 使用Container::addContainer(),它接受任何PSR-11兼容的容器,并使其中可用的所有服务在应用程序容器中可通过访问

简单的服务提供商示例

该软件包附带的是一个带有基本功能的PSR-11容器,用于在幕后使用Pimple 添加服务。

除了两个PSR-11方法外,容器还具有以下方法

  • Container::addService()通过ID添加服务工厂回调。传递给此方法的工厂将只调用一次,然后每次调用Container::get()时,都将返回相同的实例。幕后使用Pimple\Container::offsetSet()
  • Container::addFactory()通过ID添加服务工厂回调,但传递给此方法的工厂将在每次调用Container::get()时始终被调用,并返回不同的实例。幕后使用Pimple\Container::factory()
  • Container::extendService()通过添加一个回调来添加到容器中的服务,并返回相同服务的修改版本。幕后使用Pimple\Container::extend()
<?php
namespace AcmeInc\Redirector;

use Inpsyde\App\Container;
use Inpsyde\App\Provider\Booted;

final class Provider extends Booted {
    
    private const CONFIG_KEY = 'REDIRECTOR_CONFIG';
   
    public function register(Container $container): bool
    {
        // class names are used as service ids...
      
        $container->addService(
            Config::class,
            static function (Container $container): Config {
                return Config::load($container->config()->get(self::CONFIG_KEY));
            }
        );
        
        $container->addService(
            Redirector::class,
            static function (Container $container): Redirector {
                return new Redirector($container->get(Config::class));
            }
        );
        
        return true;
    }
    
    public function boot(Container $container): bool
    {
        return add_action(
            'template_redirect',
            static function () use ($container) {
                /** @var AcmeInc\Redirector\Redirector $redirector */
                $redirector = $container->get(Redirector::class);
                $redirector->redirect();
            }
        );
    }
}

使用任何PSR-11容器的服务提供商示例

在下面的示例中,我将使用PHP-DI,但任何PSR-11兼容的容器都可以。

<?php
namespace AcmeInc\Redirector;

use Inpsyde\App\Provider\Booted;
use Inpsyde\App\Container;

final class Provider extends Booted {
   
    public function register(Container $container): bool
    {
        $diBuilder = new \DI\ContainerBuilder();
        
        if ($container->config()->isProduction()) {
            $cachePath = $container->config()->get('ACME_INC_CACHE_PATH');
            $diBuilder->enableCompilation($cachePath);
        }
        
        $defsPath = $container->config()->get('ACME_INC_DEFS_PATH');
        $diBuilder->addDefinitions("{$defsPath}/redirector/defs.php");
        
        $container->addContainer($diBuilder->build());
        
        return true;
    }
    
    public function boot(Container $container): bool
    {
        return add_action(
            'template_redirect',
            static function () use ($container) {
                /** @var AcmeInc\Redirector\Redirector $redirector */
                $redirector = $container->get(Redirector::class);
                $redirector->redirect();
            }
        );
    }
}

请参阅PHP-DI文档以更好地理解代码,但再次提醒,任何PSR-11兼容的容器都可以“推入”库容器。

网站级别提供者

App::new()返回一个App实例,这样就可以现场添加提供者,而无需挂钩App::ACTION_ADD_PROVIDERS

这允许立即添加在网站级别提供的提供者。

namespace AcmeInc;

\Inpsyde\App\App::new()
    ->addProvider(new SomeWebsiteProvider())
    ->addProvider(new AnotherWebsiteProvider());

提供者包

在许多情况下,使用此包时,需要创建一个“包”,该包“集合”仅限于提供者。由于它不是插件或MU插件,因此此包需要手动“加载”,因为WordPress不会加载它,并且使用自动加载来实现此目的并不真正可行,因为使用“文件”自动加载策略,文件会在WP环境加载之前加载。

处理此问题的建议方法是从启动应用程序的应用相同的MU插件中“加载”该包。为了简化此工作流程,该包提供了一个ServiceProviders类,该类类似于提供者集合。

例如,假设我们正在创建一个提供授权系统的应用程序的包。

我们之所以会创建“库”而不是插件,是因为它不应该有任何“停用”的方式,因为它是网站的核心功能,并且其他插件和库也需要它作为依赖项。

我们在包中要做的就是创建一个包类,该类将实现Inpsyde\App\Provider\Package:一个只有一个方法的接口:Package::providers()

<?php
namespace AcmeInc\Auth;

use Inpsyde\App\Provider;
use Inpsyde\WpContext;

class Auth implements Provider\Package
{
    public function providers(): Provider\ServiceProviders
    {
        return Provider\ServiceProviders::new()
            ->add(new CoreProvider(), WpContext::CORE)
            ->add(new AdminProvider(), WpContext::BACKOFFICE, WpContext::AJAX)
            ->add(new RestProvider(), WpContext::REST, WpContext::AJAX)
            ->add(new FrontProvider(), WpContext::FRONTOFFICE, WpContext::AJAX);
    }
}

有了这样的类(并且是可自动加载的),在启动应用程序的MU插件中,我们可以这样做

<?php
namespace AcmeInc;

\Inpsyde\App\App::new()->addPackage(new Auth\Auth());

高级主题

自定义启动钩子

在README的多个地方已经说过,App::boot()最后一次调用是在init时。

但现实是,这只是一个默认设置,即使在很多情况下这没问题,实际上可以使用在最后“循环”后运行的任何plugins_loaded钩子,只需记住

  • 使用早于after_setup_theme的任何东西意味着主题将无法添加提供者。
  • 使用晚钩,添加的提供者boot()方法将无法向所选钩子之前发生的事情添加钩子,这大大减少了它们的可能性

无论如何,自定义“最后一步”钩子的方法是通过调用App::runLastBootAt()方法

<?php
namespace AcmeInc;

\Inpsyde\App\App::new()
    ->runLastBootAt('after_setup_theme')
    ->boot();

请注意,App::runLastBootAt()必须在首次调用App::boot()之前调用,否则将抛出异常。

提前构建自定义容器

有时可能希望为App使用预构建的容器。这可以更容易地使用不同的SiteConfig实例(其中EnvConfig是一个实现),或者将容器传递给服务提供者之前添加任意PSR-11容器。

这是通过创建一个App\Container实例,向其中添加一个(或多个)PSR-11容器(通过Container::addContainer方法),最后将其传递给App\App::new来实现的。例如

<?php
namespace AcmeInc;

use Inpsyde\App;

// An helper to create App on first call, then always access same instance
function app(): App\App
{
    static $app;
    
    if (!$app) {
        $env = new App\EnvConfig(__NAMESPACE__ . '\\Config', __NAMESPACE__); 
        
        // Build the App container using custom config class
        $container = new App\Container($env);
        
        // Create PSR-11 container and push into the App container
        $diBuilder = new \DI\ContainerBuilder();
        $diBuilder->addDefinitions('./definitions.php');
        $container->addContainer($diBuilder->build());
        
        // Instantiate the app with the container
        $app = App\App::new($container);
    }
    
    return $app;
}

// Finally create and bootstrap app
add_action('muplugins_loaded', [app(), 'boot']);

解决提供者之外的对象

App类有一个静态的App::make()方法,可以用来从容器外部访问对象。

这可以在只想“快速”访问容器中的服务而不编写提供者的插件中使用。

$someService = App::make(AcmeInc\SomeService::class);

因为方法是静态的,所以它需要引用App的已启动实例。将被使用的是在请求期间第一个App实例。

考虑到大多数情况下只有一个应用程序,这是很好且方便的,因为它允许在无法访问容器或App实例的情况下解决容器中的服务。

如果调用App::make()之前没有创建任何App,将抛出异常。

在这种情况下,如果由于任何原因创建了多个App实例,为了在特定的App实例中解决服务,有必要访问它并调用其上的resolve()方法。

假设在前面的部分中定义了app()函数的代码,我们可以这样做来解决服务

$someService = app()->resolve(AcmeInc\SomeService::class);

调试信息

WP_DEBUGtrue时,App类会收集有关添加的提供者和它们的状态的信息。

App::debugInfo()在调试开启时,将返回一个数组,可能如下所示

[
    'status' => 'Done with themes'
    'providers' => [
        'AcmeInc\FooProvider' => 'Registered (Registered when registering early),
        'AcmeInc\BarProvider' => 'Booted (Registered when registering early, Booted when booting early),
        'AcmeInc\CliProvider' => 'Skipped (Skipped when registering plugins)',
        'AcmeInc\LoremProvider' => 'Booted (Booted when booting plugins)',
        'AcmeInc\IpsumProvider' => 'Booted (Registered when registering plugins, Booted when booting themes),
        'AcmeInc\DolorProvider' => 'Booted (Registered when registering themes, Booted when booting themes),
        'AcmeInc\SicProvider' => 'Registered (Registered when registering themes),
        'AcmeInc\AmetProvider' => 'Booted (Registered with delay when registering themes, Booted when booting themes),
    ]
]

当调试关闭时,App::debugInfo()返回null

为了在WP_DEBUGfalse的情况下强制启用调试,可以调用App::enableDebug()

也可以通过App::disableDebug()强制关闭调试,即使WP_DEBUGtrue

<?php
namespace AcmeInc;

\Inpsyde\App\App::new()->enableDebug();

安装

使用此包的最佳方式是通过Composer

$ composer require inpsyde/wp-app-container

许可

此存储库是免费软件,并根据GNU通用公共许可证版本2或(根据您的选择)任何更高版本发布。有关完整的许可证,请参阅LICENSE

贡献

欢迎所有反馈/错误报告/拉取请求。

在发送PR之前,请确保composer run qa不会输出错误。

它将依次运行