返回
Craft CMS 4 插件开发:解决 Ajax 请求问题
php
2025-03-11 14:29:28
Craft CMS 4 插件开发:解决 Ajax 请求无法触发自定义方法的问题
问题
我在 Craft CMS 4 中创建了一个自定义插件,用于解析 JSON 数据并将其存储到相应的条目中。 起初我在 init
函数中调用了解析方法,导致每次页面加载都会触发,这显然没必要。 现在我希望通过点击 Twig 模板中的按钮来触发该方法,通过 Ajax 发送请求。 但不知道为啥,就是无法触发实际的函数。 代码如上所示。
问题分析
问题可能出在几个方面:
- 路由配置错误: Craft CMS 通过路由将 URL 请求映射到相应的控制器和方法。 你的
EVENT_REGISTER_CP_URL_RULES
事件监听器可能配置不正确,导致请求无法到达目标方法。 路由规则'_jsonify/import/test'
很奇怪。 - 控制器方法命名: 控制器的方法应该以
action
开头,Craft CMS才能正确识别. 你的控制器方法test
和actionHandleJsonRead
没有用action
作为前缀。 并且是private
私有方法,外部无法访问. - 表单提交方式: Twig 模板中的表单可能没有正确地将请求发送到预期的控制器和方法。你需要确认
actionInput
的值是否正确。 - CSRF Token: Craft CMS 使用 CSRF 保护来防止跨站请求伪造攻击。 如果没有正确处理 CSRF Token,Ajax 请求可能会被阻止。
- 插件入口函数
init()
中的额外逻辑 : 不应该在插件的init()
方法中直接执行getJsonFile
和decodeJsonFile
, 它们应该被整合到控制器方法中.
解决方案
针对以上分析,我们逐一解决这些问题:
1. 修改路由配置
将 Plugin.php
中的路由配置更改为更标准的 Craft CMS 路由格式:
// Plugin.php
use Craft;
use craft\events\RegisterCpNavItemsEvent;
use craft\web\twig\variables\Cp;
use craft\web\View;
use craft\events\RegisterTemplateRootsEvent;
use craft\events\RegisterUrlRulesEvent;
use craft\web\UrlManager;
use yii\base\Event;
public function init(): void
{
parent::init();
// Defer further initialization until Craft is fully initialized
Craft::$app->onInit(function() {
$this->attachEventHandlers();
// ... other initialization tasks ...
});
}
private function attachEventHandlers(): void
{
// Register a custom template root path
Event::on(
View::class,
View::EVENT_REGISTER_SITE_TEMPLATE_ROOTS,
function(RegisterTemplateRootsEvent $event) {
$event->roots['_jsonify'] = __DIR__ . '/templates';
}
);
// 注册侧边栏
Event::on(
Cp::class,
Cp::EVENT_REGISTER_CP_NAV_ITEMS,
function(RegisterCpNavItemsEvent $event) {
$event->navItems[] = [
'url' => '_jsonify/index', // 确保这个 URL 与下面的路由匹配
'label' => 'Jsonify',
'icon' => '@_jsonify/icon.svg',
];
}
);
// Register the event that should be triggered on this url.
Event::on(
UrlManager::class,
UrlManager::EVENT_REGISTER_CP_URL_RULES,
function(RegisterUrlRulesEvent $event) {
$event->rules['_jsonify/index'] = '_jsonify/import/index'; // twig页面渲染
$event->rules['POST _jsonify/read-json'] = '_jsonify/import/handle-json-read'; //处理按钮点击post事件.
}
);
}
- 使用
EVENT_REGISTER_CP_NAV_ITEMS
将插件入口添加到侧边导航栏。 icon.svg
文件放置在src/
或templates/
目录下, 或者使用绝对路径.
2. 修改控制器方法
修改 JsonifySettingsController.php
:
<?php
namespace plugins\jsonify\controllers;
use Craft;
use craft\web\Controller;
use craft\elements\Entry;
use yii\web\Response;
use craft\web\Request;
class ImportController extends Controller
{
protected array|bool|int $allowAnonymous = ['index', 'handle-json-read']; //允许匿名访问, 用于测试.
// 渲染主页面
public function actionIndex(): Response|string
{
$folderId = 1; // 假设你的 JSON 文件存储在 ID 为 1 的文件夹中
$assets = Craft::$app->assets->findFilesAndFolders(['folderId' => $folderId, 'kind' => 'json']);
return $this->renderTemplate('_jsonify/index', [ // '_jsonify/index' 与你设置的 template roots 对应
'assets' => $assets,
]);
}
// 处理 JSON 读取的请求
public function actionHandleJsonRead(): Response
{
// 添加 CSRF 验证 (如果不是 CLI 请求)
if (!Craft::$app->getRequest()->getIsConsoleRequest())
{
$this->requirePostRequest();
Craft::$app->getRequest()->validateCsrfToken();
}
$jsonFile = $this->getJsonFile(); //从assets里选择了一个文件
$dataArray = $this->decodeJsonFile($jsonFile); //对文件内容解码
$section = Craft::$app->sections->getSectionByHandle('test'); // 确保你有这个 section
if (!$section) {
return $this->asJson(['success' => false, 'message' => 'Section "test" not found.']);
}
$successCount = 0;
$skipCount = 0;
$errorMessages = [];
foreach ($dataArray as $data) {
$existingEntry = Entry::find()
->sectionId($section->id)
->andWhere(['title' => $data['Trial_name']])
->one();
if ($existingEntry) {
$skipCount++;
continue; // 跳过重复的
}
$entry = new Entry();
$entry->sectionId = $section->id;
$entry->typeId = $section->getEntryTypes()[0]->id; // 获取section的第一个Entry Type
$entry->authorId = 1; // 设置作者 ID,通常是管理员
$entry->title = $data['Trial_name'];
$entry->setFieldValues([ //设置你的自定义字段.
'testId' => $data['testID'],
'entryName' => $data['Name'],
'contractorSampleAnalysis' => $data['Contractor_sample_analysis'],
'country' => $data['Country'],
'crops' => $data['Crops'],
'deltaYield' => $data['Delta_yield'],
'lat' => $data['lat'],
'location' => $data['Location'],
'locationXy' => $data['location_XY'],
'lon' => $data['lon'],
'mapExport' => $data['Map_export'],
'primaryCompany' => $data['Primary_company'],
'primaryContact' => $data['Primary_contact'],
'sizeHa' => $data['Size_ha'],
'specsTrial' => $data['Specs_trial'],
'entryStatus' => $data['Status'],
'summaryResults' => $data['Summary_results'],
'testType' => $data['Test_type'],
'variety' => $data['Variety'],
'year' => $data['Year'],
'entryId' => $data['ID'],
'internalSpecsTrial' => $data['Internal_specs_trial'],
'prio' => $data['Prio'],
'trialName' => $data['Trial_name'],
'xy' => $data['XY'],
'internalStatusTodo' => $data['Internal_status_todo'],
'samplingAnalysis' => $data['Sampling_Analysis'],
'statusObservations' => $data['Status_Observations'],
'subsidyProject' => $data['Subsidy_project'],
'xyRandomiser' => $data['XY_randomiser'],
]);
if (Craft::$app->elements->saveElement($entry)) {
$successCount++;
} else {
$errorMessages[] = 'Entry: "' . $entry->title .'" Error: ' . implode(', ', $entry->getErrorSummary(true));
}
}
// 构造响应消息
$message = "$successCount entries created.";
if ($skipCount > 0) {
$message .= " $skipCount entries skipped (duplicates).";
}
if (!empty($errorMessages)) {
$message .= " Errors: " . implode(";",$errorMessages);
return $this->asJson(['success' => false, 'message' => $message ]);
}
return $this->asJson(['success' => true, 'message' => $message]);
}
//假设读取的JSON文件放在资源文件夹里, ID=1. 实际中,可以根据form表单传入 assetId来处理不同的文件.
private function getJsonFile()
{
$folderId = 1;
$assets = Craft::$app->assets->findFilesAndFolders(['folderId' => $folderId, 'kind' => 'json']);
//选择第一个文件.
if(!empty($assets) && isset($assets[0]))
{
return $assets[0];
}
return null;
}
/**
* @param $jsonAsset craft\elements\Asset
*/
private function decodeJsonFile($jsonAsset)
{
if(!$jsonAsset)
{
return [];
}
$filePath = Craft::$app->getAssets()->getAssetFileAbsolutePath($jsonAsset);
//检查文件是否存在并且可以读取
if (file_exists($filePath) && is_readable($filePath))
{
$jsonContent = file_get_contents($filePath);
try {
$data = json_decode($jsonContent, true, 512, JSON_THROW_ON_ERROR); // 使用 JSON_THROW_ON_ERROR 来捕获解码错误
return $data;
} catch (\JsonException $e) {
// 处理 JSON 解码错误,这里直接返回空, 可改为Craft Log 写入.
// Craft::error('JSON decoding error: ' . $e->getMessage(), __METHOD__);
return [];
}
}
else{
//处理读取错误.
return [];
}
}
}
- 所有公开的方法都以
action
为前缀。 - 添加了
protected array|bool|int $allowAnonymous = true;
以允许匿名访问handle-json-read
操作进行测试(实际生产环境中,应该移除或者设置为false, 进行更严格的权限控制)。 - 使用了
decodeJsonFile
方法来安全地读取和解码 JSON 数据,加入了错误处理。 - 新增了
actionIndex
来显示主页。 test
方法可以删除掉了。requirePostRequest()
和validateCsrfToken()
保证安全性。
3. 更新 Twig 模板
{# templates/_jsonify/index.twig #}
{% extends "_layouts/cp.twig" %}
{% set title = "Jsonify"|t('_jsonify') %}
{% set fullPageForm = true %}
{% block content %}
{% if assets|length %}
<ul>
{% for asset in assets %}
{% if asset.kind == 'file' %}
<li style="width: auto; display: flex; justify-content: space-between; align-items: center">
{{ asset.filename }}
<button
style="margin-left: 30px; background: #d3d3d3; padding: 3px 12px; border-radius: 4px"
class="json-button"
data-asset-id="{{ asset.id }}"
>
Read JSON
</button>
</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p>No assets found.</p>
{% endif %}
<script>
document.querySelectorAll('.json-button').forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
let assetId = this.dataset.assetId; // 获取 data-asset-id 属性
let data = {
[Craft.csrfTokenName]: Craft.csrfTokenValue, // 包含 CSRF token
assetId : assetId
}
fetch('{{ url("_jsonify/read-json") }}', { // 使用 url() 函数生成 URL,而不是 actionInput
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded', //经典form表单格式,简单.
'X-Requested-With': 'XMLHttpRequest', // 告诉 Craft 这是一个 AJAX 请求
},
body: new URLSearchParams(data).toString()
})
.then(response => response.json())
.then(data => {
// 在这里处理响应数据, 显示消息.
alert(data.message);
})
.catch((error) => {
console.error('Error:', error);
alert('An error occurred. Please check the console for details.'); //可以更友好提示.
});
});
});
</script>
{% endblock %}
- 使用 JavaScript 和
fetch
API 来发送 AJAX 请求。 - 从按钮的
data-asset-id
获取assetId
,通过POST请求传给服务器。实际情况请酌情处理,保证安全。 actionInput
被删除,使用标准的url()
函数。- 使用了
URLSearchParams
来构建post 数据。 - 正确包含了 CSRF Token。
- 添加了 JavaScript 代码来处理按钮点击事件,发送 AJAX 请求,并处理响应。
- 通过
headers
中的X-Requested-With
:XMLHttpRequest
, 告诉Craft这是一个Ajax 请求.
4. 安全建议
- 输入验证: 始终验证和清理从外部源(如 JSON 文件)接收到的数据。防止恶意数据注入。
- 错误处理: 完善错误处理机制,记录详细的错误日志,方便调试和排查问题。
- 权限控制: 生产环境下,限制对
actionHandleJsonRead
方法的访问。 - CSRF : 使用 Craft 提供的 CSRF 防护,并在前端发送请求时包含 CSRF token.
- X-Requested-With : 在Ajax请求的headers里加上
X-Requested-With
:XMLHttpRequest
, 防止某些类型的跨站攻击。
进阶使用技巧
- 使用Craft 内置的队列功能. 如果JSON文件特别大,可以把解析和数据导入的过程放入 Craft CMS 的队列 (Queue) 中,防止阻塞主线程,提升用户体验。
- 错误信息方面, 可以使用
Flash messages
给用户更友好的操作提示.
通过以上修改,你的 Craft CMS 4 插件应该可以正常工作了。 点击按钮,就能触发 actionHandleJsonRead
方法来读取和处理 JSON 数据,并将数据正确导入到 Craft CMS 的条目中.