heimrichhannot / contao-ajax-bundle
保持contao的ajax请求井然有序,并轻松处理响应。
Requires
- php: ^8.2
- contao/core-bundle: ^4.13 || ^5.0
- heimrichhannot/contao-utils-bundle: ^2.217 || ^3.0
- symfony/http-foundation: ^5.4 || ^6.0
README
Contao Ajax Bundle
默认情况下,contao中的ajax请求并未集中。由于不同类型的模块中ajax请求的处理方式不同,简单的 \Environment::get('isAjaxRequest') 并不足以将请求委派给相关模块/方法。本模块提供了一个全局配置,您可以在 $GLOBALS['AJAX']
中将您的模块作为一个自定义组附加,并添加带有所需参数的操作,这些参数应与请求进行核对。
技术说明
以下部分将向您展示如何注册和触发自定义ajax操作。
1. 配置/设置
以下示例显示了 [heimrichhannot/contao-formhybrid] (https://github.com/heimrichhannot/contao-formhybrid) 的ajax配置。
config.php
/**
* Ajax Actions
*/
$GLOBALS['AJAX'][\HeimrichHannot\FormHybrid\Form::FORMHYBRID_NAME] = array
(
'actions' => array
(
'toggleSubpalette' => array
(
'arguments' => array('subId', 'subField', 'subLoad'),
'optional' => array('subLoad'),
),
'asyncFormSubmit' => array
(
'arguments' => array(),
'optional' => array(),
),
'reload' => array
(
'arguments' => array(),
'optional' => array(),
'csrf_protection' => true, // cross-site request forgery (ajax token check)
),
),
);
如您所见,我们有一个名为 formhybrid
的组,将所有带有此 group
参数的ajax请求委派给formhybrid。然后有一些操作 toggleSubpalette
、asyncFormSubmit
等。这些方法必须在委托的上下文中以相同的名称存在。您可以提供应在函数内部调用并作为参数添加到方法中的参数。如果参数是 optional
,则请求在没有参数的情况下有效,否则所有 arguments
必须在请求中存在,才能使ajax请求有效。如果您想保护ajax请求不受跨站违规的影响,则将 csrf_protection => true
添加到您的配置中,并不要忘记在每次请求时更新ajax url!
2. 如何创建我的ajax操作的url?
我们在 HeimrichHannot\AjaxBundle\Manager\AjaxActionManager
中提供了一个简单的辅助方法,称为 generateUrl
。以下示例显示了如何在 FormHybrid
中创建 toggleSubpalette
url。
\Contao\System::getContainer()->get('huh.ajax.action')->generateUrl(Form::FORMHYBRID_NAME, 'toggleSubpalette')
如您所见,我们默认不添加参数。这是在 toggleSubpalette
的相关javascript代码中完成的。您必须在您的javascript代码中检查,以确保参数 subId
、subField
、subLoad
作为您的$_POST参数在ajax请求中由您自己提供。
jquery.formhybrid.js
toggleSubpalette: function (el, id, field, url) {
el.blur();
var $el = $(el),
$item = $('#' + id),
$form = $el.closest('form'),
checked = true;
var $formData = $form.serializeArray();
$formData.push(
{name: 'FORM_SUBMIT', value: $form.attr('id')},
{name: 'subId', value: id},
{name: 'subField',value: field});
if ($el.is(':checkbox') || $el.is(':radio')) {
checked = $el.is(':checked');
}
if (checked === false) {
$.ajax({
type: 'post',
url: url,
dataType: 'json',
data: $formData,
success: function (response) {
$item.remove();
}
});
return;
}
$formData.push(
{name: 'subLoad', value: 1}
);
$.ajax({
type: 'post',
url: url,
dataType: 'json',
data: $formData,
success: function (response, textStatus, jqXHR) {
$item.remove();
// bootstrapped forms
if ($el.closest('form').find('.' + field).length > 0) {
// always try to attach subpalette after wrapper element from parent widget
$el.closest('form').find('.' + field).eq(0).after(response.result.html);
} else {
$el.closest('#ctrl_' + field).after(response.result.html);
}
}
});
}
3. 我的ajax操作是如何触发的?
为了给您最大的自由度,我们决定始终处于contao上下文中。因此,所有请求前提条件都在默认的contao请求周期中进行检查。
在 toggleSubpalette
示例中,我们在 DC_Hybrid::__construct()
中触发操作,并提供一个 new FormAjax($this)
作为响应上下文。
注意:不要在Contao模块的generate()中调用"runActiveAction",因为那太晚了。最好在__construct()中运行它。
DC_Hybrid.php
public function __construct($strTable = '', $varConfig = null, $intId = 0)
{
...
\Contao\System::getContainer()->get('huh.ajax')->runActiveAction(Form::FORMHYBRID_NAME, 'toggleSubpalette', new FormAjax($this));
...
}
那么这里做了什么?
- 您的javascript代码请求ajax操作。
- ajax操作被委派到当前页面,其中我们的
DC_Hybrid
扩展模块可用。 Ajax
控制器将请求与$GLOBALS['AJAX']
配置中给定的toggleSubpalette
参数进行核对。- 如果所有要求都满足,并且方法
toggleSubpalette
在给定的上下文new FormAjax($this)
中可用,则使用给定的参数触发该方法。 - 现在您可以在
toggleSubpalette
中执行模块相关的操作 - 如果您想向ajax请求返回数据,则必须返回一个有效的
HeimrichHannot\AjaxBundle\Response\Response
对象。 - 返回的
HeimrichHannot\AjaxBundle\Response\Response
对象将被转换为 JSON 对象,请求将在这里结束。
响应对象
目前我们实现了三个响应对象。
HeimrichHannot\AjaxBundle\Response\ResponseSuccess
HeimrichHannot\AjaxBundle\Response\ResponseError
HeimrichHannot\AjaxBundle\Response\ResponseRedirect
ResponseSuccess
这将返回一个包含 HTTP 状态码 HTTP/1.1 200 OK
的 JSON 对象给 AJAX 动作。
示例
客户端
$.ajax({
type: 'post',
url: url,
dataType: 'json',
data: $formData,
success: function (response, textStatus, jqXHR) {
$item.remove();
// bootstrapped forms
if ($el.closest('form').find('.' + field).length > 0) {
// always try to attach subpalette after wrapper element from parent widget
$el.closest('form').find('.' + field).eq(0).after(response.result.html);
} else {
$el.closest('#ctrl_' + field).after(response.result.html);
}
}
});
服务器端
/**
* Toggle Subpalette
* @param $id
* @param $strField
* @param bool $blnLoad
*
* @return ResponseError|ResponseSuccess
*/
function toggleSubpalette($id, $strField, $blnLoad = false)
{
$varValue = \Contao\System::getContainer()->get('huh.request')->getPost($strField) ?: 0;
if (!is_array($this->dca['palettes']['__selector__']) || !in_array($strField, $this->dca['palettes']['__selector__'])) {
\Controller::log('Field "' . $strField . '" is not an allowed selector field (possible SQL injection attempt)', __METHOD__, TL_ERROR);
return new ResponseError();
}
$arrData = $this->dca['fields'][$strField];
if (!Validator::isValidOption($varValue, $arrData, $this->dc)) {
\Controller::log('Field "' . $strField . '" value is not an allowed option (possible SQL injection attempt)', __METHOD__, TL_ERROR);
return new ResponseError();
}
if (empty(FormHelper::getFieldOptions($arrData, $this->dc))) {
$varValue = (intval($varValue) ? 1 : '');
}
$this->dc->activeRecord->{$strField} = $varValue;
$objResponse = new ResponseSuccess();
if ($blnLoad)
{
$objResponse->setResult(new ResponseData($this->dc->edit(false, $id)));
}
return $objResponse;
}
ResponseError
这将返回一个包含 HTTP 状态码 HTTP/1.1 400 Bad Request
的 JSON 对象给 AJAX 动作。
示例
客户端
$.ajax({
url: url ? url : $form.attr('action'),
dataType: 'json',
data: $formData,
method: $form.attr('method'),
error: function(jqXHR, textStatus, errorThrown){
if (jqXHR.status == 400) {
alert(jqXHR.responseJSON.message);
return;
}
}
});
服务器端
$objResponse = new ResponseRedirect();
$objResponse->setUrl($strUrl);
return $objResponse;
ResponseRedirect
这将返回一个包含 HTTP 状态码 HTTP/1.1 301 Moved Permanently
的 JSON 对象给 AJAX 动作。重定向 URL 包含在 xhr 响应对象 result.data.url
中;
示例
客户端
$.ajax({
url: url ? url : $form.attr('action'),
dataType: 'json',
data: $formData,
method: $form.attr('method'),
error: function(jqXHR, textStatus, errorThrown){
if (jqXHR.status == 301) {
location.href = jqXHR.responseJSON.result.data.url;
return;
}
}
});
服务器端
$objResponse = new ResponseRedirect();
$objResponse->setUrl($strUrl);
die(json_encode($objResponse));
单元测试
对于单元测试,在 $GLOBALS 中将变量 UNIT_TESTING
设置为 true
。
//bootstrap.php
define('UNIT_TESTING', true);
然后你可以通过捕获 HeimrichHannot\AjaxBundle\Exception\AjaxExitException
来捕获你的测试中的 AJAX 结果。
// MyTestClass.php
/**
* @test
*/
public function myTest()
{
$objRequest = \Contao\System::getContainer()->get('huh.request')->create('http://localhost' . AjaxAction::generateUrl('myAjaxGroup', 'myAjaxAction'), 'post');
$objRequest->headers->set('X-Requested-With', 'XMLHttpRequest'); // xhr request
Request::set($objRequest);
$objForm = new TestPostForm();
try
{
$objForm->generate();
// unreachable code: if no exception is thrown after form was created, something went wrong
$this->expectException(\HeimrichHannot\Ajax\Exception\AjaxExitException::class);
} catch (AjaxExitException $e)
{
$objJson = json_decode($e->getMessage());
$this->assertTrue(strpos($objJson->result->html, 'id="my_css_id"') > 0); // check that id is present within response
}
}