第 8 步依赖注入
在这一步中,我们将介绍 Joomla 扩展和分配器类及依赖注入的基本原则,并说明 services/provider.php 文件。
我们将更改 mod_hello 以通过依赖注入来获取 HelloHelper 类。
本步骤非常冗长且复杂,但值得坚持,因为这些原则是 Joomla 和开发 Joomla 扩展的基础。
源代码位于 mod_hello 第 8 步。
扩展和调度程序类
Joomla 使用一种常见的方法将控件传递给扩展,而对于模块和组件来说非常类似。从高层来看,它是如何启动一个模块的
服务提供程序代码位于 services/provider.php
可以将扩展类视为 Joomla 核心代码用来连接到我们模块的“句柄”。它是最先被实例化的模块类。对于模块来说,扩展类主要用于获取调度程序类。
可以将调度程序类视为向 Joomla 提供一种运行我们的模块代码的机制,因为它提供了 Joomla 调用运行模块的 dispatch
函数。
调度程序 dispatch
函数通常是模块逻辑开始的地方。
你可能想知道为什么扩展类不只是提供 dispatch
函数本身——好吧,它可以做到,并且这可能让扩展开发人员更容易,但它是被分离出来以启用组件、模块和插件的相似一组类。
你可以在这里找到有关扩展和调度程序类的更多信息。
依赖注入
从战略上讲,Joomla 为其自己的扩展采用以下模式
- 扩展的服务提供程序文件将扩展的关键类放入依赖注入容器 (DIC)
- 然后 Joomla 从 DIC 中提取主扩展类
- 通过扩展类,扩展可访问其它的关键类
这种方法使得轻松模拟这些类以进行单元测试——你只需提供指向模拟类的测试服务提供程序文件即可。
它还提供了一个灵活性点,用于在你的网站上自定义 Joomla:Joomla 在条目标记加载到 DIC 之后但在它们被取出之前会触发一个事件。因此,你可以在插件中捕获该事件,使用你自己的标志替换扩展的 DIC 条目,并通过这种方式修改 Joomla 扩展的功能。
Joomla 的依赖注入方法在这里有更详细的描述(你可以在那里找到指向关联视频的链接)。
对于 mod_hello,有 3 个类是通过 DIC 获取的
- 扩展类 - 这将只是针对模块的默认 Joomla 扩展类:\Joomla\CMS\Extension\Module
- 调度程序类
- 帮助程序类
同时,Joomla 模式规定我们不要将 Dispatcher 和 Helper 类直接放入 DIC 中,而应改用 DispatcherFactory 和 HelperFactory 类,并且将这两个 Factory 类传递至 Extension 类的构造函数中,并将其存储为局部实例变量 $this->dispatcherFactory
和 $this->helperFactory
。
然后,我们可以使用类似于以下内容在 Extension 类内获取 Dispatcher 和 Helper 类的新实例
public function getDispatcher(...) {
$dispatcher = $this->dispatcherFactory->createDispatcher(...);
}
public function getHelper(...) {
$helper = $this->helperFactory->getHelper(...);
}
但是,我们通常希望在 Dispatcher 代码内获取 Helper 类(而不是在里面或 Extension 类内),因为真正用来设置要在模块中显示的数据的工作是在那里完成的。
Joomla 不会为我们提供从 Dispatcher 返回至 Extension 的链接,但如果我们在 Dispatcher 内执行以下操作,它将提供指向 HelperFactory 类的链接
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
use Joomla\CMS\Helper\HelperFactoryAwareTrait;
class Dispatcher implements HelperFactoryAwareInterface
{
use HelperFactoryAwareTrait;
...
$helper = $this->getHelperFactory()->getHelper('HelloHelper');
...
}
指向 HelperFactory 的此链接在 libraries/src/Extension/Module.php 中 \Joomla\CMS\Extension\Module 的 getDispatcher
函数中,使用以下代码设置
if ($dispatcher instanceof HelperFactoryAwareInterface) {
$dispatcher->setHelperFactory($this->helperFactory);
}
同时,HelperFactoryAwareTrait 为 Dispatcher 提供了 getHelperFactory 和 setHelperFactory 函数。
服务提供者
现在我们已经能够理解服务提供程序文件了。此 PHP 文件基本上会返回一个匿名类实例,该实例实现了 Joomla\DI\ServiceProviderInterface 并具有一个公共函数 register(Container $container)
。Joomla 调用此 register
函数来在 DIC 中创建条目。
在 register
函数内,我们可以使用以下方法为 DispatcherFactory 在 DIC 中创建一个条目
$container->set(
ModuleDispatcherFactoryInterface::class,
function (Container $container) {
return new \Joomla\CMS\Dispatcher\ModuleDispatcherFactory('\\My\\Module\\Hello');
}
);
ModuleDispatcherFactory 会传递进我们的命名空间,以便它可以形成我们调度器 \My\Module\Hello\Site\Dispatcher\Dispatcher 的完全限定名,因此 Joomla 将使用 PSR-4 命名空间规则 在 modules/mod_hello/src/Dispatcher/Dispatcher.php 中找到我们的源代码文件。
HelperFactory 同理
$container->set(
HelperFactoryInterface::class,
function (Container $container) {
return new \Joomla\CMS\Helper\HelperFactory('\\My\\Module\\Hello\\Site\\Helper');
}
);
最后,针对我们的 Extension 类
use Joomla\CMS\Extension\ModuleInterface;
// inside register():
$container->set(
ModuleInterface::class,
function (Container $container) {
return new \Joomla\CMS\Extension\Module(
$container->get(ModuleDispatcherFactoryInterface::class),
$container->get(HelperFactoryInterface::class)
);
}
);
(顺便说一句,ModuleInterface::class
只是包含类或接口的完全限定名的字符串的缩写)。
当 Joomla 获取 ModuleInterface::class
的条目时,它将导致关联函数运行。这会创建 Module 实例,并且将从获取 DIC 中 ModuleDispatcherFactoryInterface::class 和 HelperFactoryInterface::class 条目返回的内容传递至构造函数中。
这 3 个 DIC 条目使用如此频繁,以至于 Joomla 提供了一种将它们输入到 DIC 中的简便方法。我们将其用于我们的服务提供程序文件中
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\Module as ModuleServiceProvider;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory as ModuleDispatcherFactoryServiceProvider;
use Joomla\CMS\Extension\Service\Provider\HelperFactory as HelperFactoryServiceProvider;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactoryServiceProvider('\\My\\Module\\Hello'));
$container->registerServiceProvider(new HelperFactoryServiceProvider('\\My\\Module\\Hello\\Site\\Helper'));
$container->registerServiceProvider(new ModuleServiceProvider());
}
};
此处 registerServiceProvider
仅仅表示对服务提供程序类实例(它已传入)调用 register
。
对于用于服务提供者类和工厂类的名称 ModuleDispatcherFactory,需要检查 use
语句以确认其含义。其他工厂服务提供者类也是如此。
Helper 类
到目前为止,我们一直在使用以下内容获取 HelloHelper 类
use My\Module\Hello\Site\Helper\HelloHelper;
$username = HelloHelper::getLoggedonUsername('Guest');
如果我们希望对 Dispatcher 类进行单元测试,并希望为 HelloHelper 函数提供模拟,我们该怎么办?
由于 Joomla 使用 PSR-4 规则查找 HelloHelper 类——我们无法轻松中断的机制——这意味着我们不得不将代码更改为类似于
use My\Mocks\HelloHelper;
$username = HelloHelper::getLoggedonUsername('Guest');
因此,我们正在更改我们正在尝试进行单元测试的源文件本身。我们可以通过使用依赖注入来避免这种情况。
如果我们使用依赖注入,那么我们的 Dispatcher 代码就是
class Dispatcher implements HelperFactoryAwareInterface
{
use HelperFactoryAwareTrait;
$username = $this->getHelperFactory()->getHelper('HelloHelper')->getLoggedonUsername('Guest');
...
}
此处没有对 HelloHelper 类的直接引用。如果我们希望模拟 HelloHelper 类,则可以将服务提供程序文件从
$container->registerServiceProvider(new HelperFactoryServiceProvider('\\My\\Module\\Hello\\Site\\Helper'));
更改为
$container->registerServiceProvider(new HelperFactoryServiceProvider('\\My\\Mocks'));
然后 HelperFactory 将改为实例化我们的模拟 HelloHelper,并且我们不必更改我们正在进行单元测试的文件的代码。
更新的 Helper 代码
由于 HelloHelper 类由 HelperFactory 实例化,因此我们必须从 getLoggedonUsername
函数中移除 static
<?php
namespace My\Module\Hello\Site\Helper;
\defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class HelloHelper
{
public function getLoggedonUsername(string $default)
{
$user = Factory::getApplication()->getIdentity();
if ($user->id !== 0) // found a logged-on user
{
return $user->username;
}
else
{
return $default;
}
}
}
更新的 Dispatcher 代码
<?php
namespace My\Module\Hello\Site\Dispatcher;
\defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\DispatcherInterface;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\Input\Input;
use Joomla\Registry\Registry;
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
use Joomla\CMS\Helper\HelperFactoryAwareTrait;
class Dispatcher implements DispatcherInterface, HelperFactoryAwareInterface
{
use HelperFactoryAwareTrait;
protected $module;
protected $app;
public function __construct(\stdClass $module, CMSApplicationInterface $app, Input $input)
{
$this->module = $module;
$this->app = $app;
}
public function dispatch()
{
$language = $this->app->getLanguage();
$language->load('mod_hello', JPATH_BASE . '/modules/mod_hello');
$username = $this->getHelperFactory()->getHelper('HelloHelper')->getLoggedonUsername('Guest');
$hello = Text::_('MOD_HELLO_GREETING') . $username;
$params = new Registry($this->module->params);
require ModuleHelper::getLayoutPath('mod_hello');
}
}
Manifest 文件
<?xml version="1.0" encoding="UTF-8"?>
<extension type="module" client="site" method="upgrade">
<name>MOD_HELLO_NAME</name>
<version>1.0.8</version>
<author>me</author>
<creationDate>today</creationDate>
<description>MOD_HELLO_DESCRIPTION</description>
<namespace path="src">My\Module\Hello</namespace>
<files>
<folder module="mod_hello">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
<folder>language</folder>
</files>
<scriptfile>script.php</scriptfile>
<media destination="mod_hello" folder="media">
<filename>joomla.asset.json</filename>
<folder>js</folder>
</media>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="header"
type="list"
label="MOD_HELLO_HEADER_LEVEL"
default="h4"
>
<option value="h3">MOD_HELLO_HEADER_LEVEL_3</option>
<option value="h4">MOD_HELLO_HEADER_LEVEL_4</option>
<option value="h5">MOD_HELLO_HEADER_LEVEL_5</option>
<option value="h6">MOD_HELLO_HEADER_LEVEL_6</option>
</field>
</fieldset>
</fields>
</config>
</extension>