跳至主要内容
版本:5.1

第 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 获取的

  1. 扩展类 - 这将只是针对模块的默认 Joomla 扩展类:\Joomla\CMS\Extension\Module
  2. 调度程序类
  3. 帮助程序类

同时,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 中的简便方法。我们将其用于我们的服务提供程序文件中

mod_hello/services/provider.php
<?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

mod_hello/src/Helper/HelloHelper.php
<?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 代码

mod_hello/src/Dispatcher/Dispatcher.php
<?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 文件

mod_hello/mod_hello.xml
<?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>