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
以运行组件。
包中的文件如下所示。
以下是每个文件的说明。
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
);
setUserState
和 getUserState
函数使用设置为 '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 模式。