跳到主要内容
版本:5.1

MVC 和其他注意事项

介绍

上一节代码的编写方式,如果你正在开发一个真正的 Joomla 组件,并不是最佳方法。相反,你应该遵循 Joomla 核心代码的设计方式,特别是将你的组件分成 MVC - 模型、视图、控制器模式。本节结尾处的修改后的组件代码 com_sample_form2 遵循了这种方法。

本节介绍 MVC 和其他设计模式,遵循这些模式通常使组件代码更容易理解,特别是在大型组件中。

Joomla MVC 拆分

一般来说,Joomla 将组件分成不同类型的功能

  • 控制器包含决定如何响应 HTTP 请求的逻辑 - 特别是,决定使用哪个视图和模型
  • 视图定义应在要呈现的网页上显示哪些数据项,并调用模型以获取这些数据项
  • 模型提供对数据的访问
  • tmpl 文件是视图的扩展(它在视图类实例的上下文中运行,因此可以直接访问视图对象的 $this 变量)。它输出组件的 HTML,并在输出中包含视图收集的数据。它与视图代码分离,以便可以使用模板覆盖轻松地覆盖 HTML 输出。

Post/Request/Get 模式

在 Joomla 中,所有 HTML 输出(例如表单的显示)都是响应 HTTP GET 执行的,遵循 Post/Redirect/Get 模式 模式。上一节的示例代码没有遵循这种模式,而是将验证错误输出并在响应 HTTP POST 请求时重新显示表单。

要遵循 Joomla 模式,在处理 POST 的代码中,我们应该包含一个指向表单 URL 的 HTTP GET 重定向。由于该 GET 随后将成为一个新的 HTTP 请求/响应,因此我们必须在用户会话中存储在重新显示表单时要显示的数据,其中包括以下 2 项

  • 使用 enqueueMessage() 方法存储并输出验证错误消息(该方法会自动为我们存储用户会话中的数据)
$app = Factory::getApplication();
$app->enqueueMessage('some message text');
  • 使用 setUserState() 存储用户输入的数据,并使用 getUserState() 检索数据,并使用对该表单唯一的上下文进行键控。例如
$app->setUserState('com_sample_form2.sample', $data);

提供表单数据的代码 bind() 操作必须首先使用 getUserState() 检查会话中是否存在任何数据,因为这将是用户先前输入的数据,应该重新显示。

此外,如果用户输入的数据成功通过验证,则应调用 setUserState() 传递 null 以清除会话中的此预填充数据;否则,它将在用户下次显示表单时出现。

当然,虽然 Joomla 使用这种模式,但你并不总是必须遵循它。例如,如果你有大量确认数据需要在成功提交表单后输出,那么你可以直接将其作为对 HTTP POST 的响应输出。

单独的控制器

Joomla 根据请求中发送的 task URL 参数的值将 HTTP 请求路由到单独的控制器。此参数通常由基于提交按钮的 Joomla 核心 javascript 设置,例如在下面的示例中

onclick="Joomla.submitbutton('myform.submit')"

单击 submit 按钮时,onclick 监听器会调用 javascript 函数 Joomla.submitbutton 传递参数 'myform.submit',这会导致 task 参数被设置为“myform.submit”。

一般来说,task 参数的形式为 <controller type>.<method>,因此在这种情况下,包含表单数据的 HTTP POST 将由 MyformController 及其 submit 方法处理。

Joomla MVC 类

Joomla 提供功能丰富的控制器、视图和模型类,你的组件控制器、视图和模型可以从这些类继承。com_sample_form2 中的模型代码继承自 FormModel,该模型在一定程度上屏蔽了上一节中介绍的 Joomla 表单 API。在这种情况下,我们的模型调用 FormModel loadForm() 方法,然后该方法将执行回调以调用我们的 loadFormData() 以向 bind() 提供数据以绑定到表单。因此,在代码中没有对 bind() 的单独调用。

安全令牌

Joomla 在表单上使用安全令牌来防止 CSRF 攻击。令牌在布局文件中输出

<?php echo HTMLHelper::_('form.token'); ?>

并在处理 POST 的控制器中进行检查

$this->checkToken();

如果发现令牌无效,则 checkToken() 会输出警告并将用户重定向回上一页。

验证

这将在以下部分介绍。

示例代码

在本节中,你可以下载 此组件 zip 文件 并安装它。它基本上与上一节中的 com_sample_form1 具有相同的功能,但已根据上述原则进行了重新设计。虽然乍一看它可能看起来更复杂,但以这种方式分配功能使代码在组件变得庞大时更容易理解。

安装文件后,导航到你的网站主页并添加查询参数 ?option=com_sample_form2 以运行组件。

包中的文件如下所示。

com_sample_form2 files

以下是每个文件的说明。

admin/services/provider.php

这只是一个与 Joomla 依赖注入 相关的标准源文件。

site/src/Controller/DisplayController.php

此类的 display() 方法是在你最初导航到 com_sample_form2 组件时运行的方法。

$model = $this->getModel('sample');
$view = $this->getView('sample', 'html');
$view->setModel($model, true);
$view->display();

$model$view 被创建,传递的参数 'sample' 指示模型和视图必须具有的完全限定名 (FQN)。换句话说,Joomla 使用此 'sample' 字符串作为确定模型和视图类的 FQN 的一部分。(这两个类实例实际上是由通过 services/provider.php 文件包含的 MVCFactory 类对象创建的。)

然后调用 setModel,以便模型对视图代码可用,传递 true 表示它是视图的默认模型。

最后,调用视图 display 方法。

site/src/View/Sample/HtmlView.php

在视图 display 函数中

$this->form = $this->getModel()->getForm();
parent::display($tpl);

它调用(默认)模型的 getForm 方法,然后调用 parent::display(),该方法基本上运行 tmpl/default.php 文件。

site/src/Model/SampleModel.php

在模型 getForm 函数中,我们有

$form = $this->loadForm(
'com_sample_form2.sample', // just a unique name to identify the form
'sample_form', // the filename of the XML form definition
// Joomla will look in the site/forms folder for this file
array(
'control' => 'jform', // the name of the array for the POST parameters
'load_data' => $loadData // if set to true, then there will be a callback to
// loadFormData to supply the data
)
);

loadForm 方法位于 libraries/src/MVC/Model/FormModel.php 中,通过它使用的 FormBehaviorTrait(位于 libraries/src/MVC/Model/FormBehaviorTrait)中。

loadForm 函数将获取一个 Joomla Form 实例,配置为使用 'control' => 'jform',然后将调用该 Form 实例上的 loadFile 以读取 site/forms/sample_form.xml 中的表单定义。

由于这种情况下的 load_data 设置为 true,因此将回调 loadFormData,其中有

$data = Factory::getApplication()->getUserState(
'com_sample_form2.sample', // a unique name to identify the data in the session
array("email" => ".@.") // prefill data if no data found in session
);

setUserStategetUserState 函数使用设置为 'com_sample_form2.sample' 的键将数据存储在 Joomla Session 中。(你可以在全局配置参数中将“调试系统”设置为是,然后在网页上单击页面左下角的 Joomla 符号,查看 Session 中的内容。)

site/tmpl/sample/default.php

<form action="<?php echo Route::_('index.php?option=com_sample_form2'); ?>"
method="post" name="adminForm" id="adminForm" enctype="multipart/form-data">

<?php echo $this->form->renderField('message'); ?>

<?php echo $this->form->renderField('email'); ?>

<?php echo $this->form->renderField('telephone'); ?>

<button type="button" class="btn btn-primary" onclick="Joomla.submitbutton('myform.submit')">Submit</button>

<input type="hidden" name="task" />
<?php echo HtmlHelper::_('form.token'); ?>
</form>

这里有几点需要注意

  • <form> 上的 action 属性表示 POST 应返回到我们的 com_sample_form2 组件
  • 每个字段都像以前一样使用 renderField 进行渲染
  • submit 按钮有一个 onclick 监听器,它会导致 task URL 参数被设置为 'myform.submit'。因此 Joomla 会将其路由到 MyformController,以及其中的 submit() 方法。
  • 我们仍然需要显式地包含一个类型为 task 的隐藏字段
  • 安全令牌包含在 HtmlHelper::_('form.token') 中。这将作为 POST 参数之一发送,我们需要在处理表单提交时进行检查。

site/src/Controller/MyformController.php

当收到 HTTP POST 请求时,Joomla 会将其路由到此控制器,并调用 `submit` 方法。

$this->checkToken();

这将检查安全令牌,如果令牌无效,则导致表单提交被拒绝。

$model = $this->getModel('sample');
$form = $model->getForm(null, false);

与 DisplayController 一样,我们获取模型。但是这里不会有视图,所以控制器必须调用 `getForm`。我们将 `false` 作为第二个参数传递,因为我们不希望它回调模型来预填充数据。

// name of array 'jform' must match 'control' => 'jform' line in the model code
$data = $this->input->post->get('jform', array(), 'array');

这将检索 HTTP POST 请求中发送的参数。

// This is validate() from the FormModel class, not the Form class
// FormModel::validate() calls both Form::filter() and Form::validate() methods
$validData = $model->validate($form, $data);

这将处理表单过滤和验证步骤。

if ($validData === false)
{
$errors = $model->getErrors();

foreach ($errors as $error)
{
if ($error instanceof \Exception)
{
$app->enqueueMessage($error->getMessage(), 'warning');
}
else
{
$app->enqueueMessage($error, 'warning');
}
}

// Save the form data in the session, using a unique identifier
$app->setUserState('com_sample_form2.sample', $data);
}
else
{
$app->enqueueMessage("Data successfully validated", 'notice');
// Clear the form data in the session
$app->setUserState('com_sample_form2.sample', null);
}

如果数据无效,则它将输出关联的错误消息,并使用 `setUserState` 在 `Session` 中存储用户在表单中输入的内容。

如果数据有效,则它将输出成功消息,并清除 `Session` 中的数据,以便下次显示表单时,它只包含正常的预填充数据。

// Redirect back to the form in all cases
$this->setRedirect(Route::_('index.php?option=com_sample_form2', false));

这遵循 Joomla 使用的 Post/Request/Get 模式。