标准#
当 XenForo 需要根据某些用户选择的条件(标准)来测试某些内容(用户/页面/帖子等)时,它会使用标准系统。
一些使用标准系统的地方包括:
- 奖杯
- 用户组晋升
- 论坛通知
插件也可以使用这个系统。
标准类型#
考虑以下标准:
- 用户有/没有头像
- 用户有超过 300 条消息
- 用户正在创建线程
- 当前用户选择的导航标签是“成员”
前两个标准涉及用户本身。其余的标准涉及用户在论坛上的当前位置。看来我们有不同类别或类型的标准。
XenForo 默认有两种标准类型:
- 用户标准 —— 处理与用户本身相关的标准
- 页面标准 —— 处理与用户当前位置相关的标准 + 时间标准
一些插件可能还会添加自己的标准类型。
从代码的角度来看,标准类型只是抽象类 AbstractCriteria
的子类。它们包含处理特定类型选定标准的代码。
AbstractCriteria
反过来提供了处理标准的一般方法,无论它们的含义如何。
标准#
标准是用户可选择的预定义条件。
为什么是可选择的? 因为管理员/用户可以选择它们(记得奖杯创建过程)。
为什么是预定义的? 因为 XenForo 已经知道如何处理它们(使用标准类方法)。
每个标准由两部分组成:规则和(可选的)数据。
规则#
标准规则只是一个 snake case 格式的字符串(单词之间用下划线分隔)。
它有两个基本目的:
- 用于区分标准
- 在执行匹配时,规则会转换为处理此标准的方法的 camel case 名称(参见 "标准如何工作")。
数据#
它只是一个可选的附加标准数据数组。例如,“用户至少发布了 X 条消息”标准有一个包含一个元素的数组:消息数量。
标准系统如何工作#
在本节中,我们将从头到尾描述标准系统的工作原理。
模板#
这一切都从模板代码开始。以下是标准在模板中的样子:
<xf:checkbox label="标准容器">
<!-- 标准 -->
<xf:option name="foo_criteria[criterion_1_rule][rule]" value="criterion_1_rule" ... />
<!-- 带数据的标准 -->
<xf:option name="bar_criteria[criterion_2_rule][rule]" value="criterion_2_rule" ... >
<xf:... name="bar_criteria[criterion_2_rule][data][param_1]" ... />
<xf:... name="bar_criteria[criterion_2_rule][data][param_2]" ... />
</xf:option>
</xf:checkbox>
如你所见,标准只是一个带有可选输入字段(标准数据)的复选框。让我们分析一下代码:
foo_criteria
和bar_criteria
是输入容器,通常foo
和bar
部分指的是标准类型。例如,user_criteria[...]
让我们知道这些标准属于用户标准。value="criterion_1_rule"
和value="criterion_2_rule"
显然是标准的规则。
注意
请记住,name
属性中的 criterion_1/2_rule
不一定是标准规则!这些只是输入容器的名称。你可以轻松地写 <xf:option name="foo[bar][rule]" value="criterion_rule" />
,它会正常工作。标准规则将是 criterion_rule
,而不是 bar
。
(可选)存储选定的标准#
在控制器中,可以过滤、编码并保存上一节中的标准表单数据到 mediumblob
类型的数据库列中,以备将来使用:
$fooCriteriaInput = $this->filter('foo_criteria', 'array');
$barCriteriaInput = $this->filter('bar_criteria', 'array');
$form->basicEntitySave($bazEntity, [
'foo_criteria' => $fooCriteriaInput,
'bar_criteria' => $barCriteriaInput
]);
示例 $bazEntity
结构:
public static function getStructure(Structure $structure)
{
$structure->table = 'xf_baz';
$structure->shortName = 'XF:Baz';
$structure->primaryKey = 'baz_id';
$structure->columns = [
'baz_id' => ['type' => self::UINT, 'autoIncrement' => true],
'foo_criteria' => ['type' => self::JSON_ARRAY, 'default' => [], 'required' => 'please_select_criteria_that_must_be_met'],
'bar_criteria' => ['type' => self::JSON_ARRAY, 'default' => []]
];
return $structure;
}
标准对象#
为了使用标准系统,我们需要从选定的标准表单数据中创建一个标准对象。这可以通过应用程序的 criteria()
方法完成:
/** @var \Qux\Criteria\Foo $fooCriteria */
$fooCriteria = \XF::app()->criteria('Qux:Foo', $bazEntity->foo_criteria);
/** @var \Qux\Criteria\Bar $barCriteria */
$barCriteria = \XF::app()->criteria('Qux:Bar', $bazEntity->bar_criteria);
从现在开始,我们可以使用所有 AbstractCriteria
功能以及我们在子类 Foo
/Bar
中额外编写的所有内容。
匹配#
当我们想要检查某物(用户)是否匹配选定的标准时,我们使用 isMatched
方法:
$visitor= \XF::visitor();
if ($fooCriteria->isMatched($visitor))
{
// 访客匹配所有选定的标准
}
else
{
// 访客不匹配一个或多个标准
}
isMatched()
将标准规则转换为带有 _match
前缀的 camel case 方法名称:criterion_1_rule
> _matchCriterion1Rule
,并尝试在标准类型类中找到这样的方法(在我们的示例中是 Foo
类):
// Qux/Criteria/Foo.php
protected function _matchCriterion1Rule(array $data, \XF\Entity\User $user)
{
/* ... 处理标准 ... */
return true; // 用户匹配当前标准
/* 或者 */
return false; // 用户不匹配当前标准
}
如果在类中找不到某些方法,isMatched()
会调用 isUnknownMatched()
,其行为可以在 AbstractCriteria
祖先中设置(默认返回 false
)。
如果没有选择任何标准,isMatched()
会返回 $matchOnEmpty
变量,默认情况下为 true
。你可以通过调用 $crteriaObj->setMatchOnEmpty(false)
来改变这种行为,在使用 isMatched()
方法之前:
$visitor= \XF::visitor();
$fooCriteria->setMatchOnEmpty(false);
if ($fooCriteria->isMatched($visitor))
{
// 访客匹配所有选定的标准
}
else
{
// 访客不匹配一个或多个标准
}
标准如何工作(示例)#
想象一下,你想奖励所有拥有头像并收到至少 5 个赞的用户一个奖杯。
在创建奖杯时,你选择“用户有头像”(规则 has_avatar
)和“用户收到至少 X 个赞”(规则 like_count
)标准。最后一个标准还有一个包含一个元素的数组:赞的数量。
你选择的标准存储在 xf_trophy
表的 user_criteria
列中。
当 XenForo 决定是否奖励用户奖杯时,它会将规则转换为 camel case 方法名称:
like_count
>_matchLikeCount()
has_avatar
>_matchHasAvatar()
由于这两个选定的标准都是用户标准,XenForo 会调用用户标准类,并尝试在其中找到这些方法:
//...
protected function _matchLikeCount(array $data, \XF\Entity\User $user)
{
return ($user->like_count && $user->like_count >= $data['likes']);
}
//...
protected function _matchHasAvatar(array $data, \XF\Entity\User $user)
{
return $user->user_id && ($user->avatar_date || $user->gravatar);
}
//...
如果所有调用的方法都返回 true
,我们的用户就匹配了选定的标准,因此将获得奖杯。
如果在用户标准类中找不到某些方法,XenForo 会调用 isUnknownMatched()
方法,该方法会触发 criteria_user
事件,允许插件开发者添加他们的自定义标准处理程序(参见 "自定义用户/页面标准示例")。
额外的标准数据#
有时,在编写标准模板代码时,你需要访问未通过视图参数传递的额外数据。
这就是 getExtraTemplateData()
方法存在的原因。默认情况下,它包含现有的用户组、语言、样式、时区。
你可以在自定义标准类型类中覆盖此方法。
在自定义标准类型中添加数据#
在你的自定义标准类中覆盖 getExtraTemplateData()
方法:
public function getExtraTemplateData()
{
$templateData = parent::getExtraTemplateData();
$additionalData = [];
/** @var \XF\Repository\Smilie $smilieRepo */
$smilieRepo = \XF::repository('XF:Smilie');
$additionalData['smilies'] = $smilieRepo->findSmiliesForList()->fetch();
return array_merge($templateData, $additionalData);
}
向现有标准类型添加数据#
你可以使用 criteria_template_data
事件监听器来添加你自己的额外标准数据:
public static function criteriaTemplateData(array &$templateData)
{
/** @var \XF\Repository\Smilie $smilieRepo */
$smilieRepo = \XF::repository('XF:Smilie');
$templateData['smilies'] = $smilieRepo->findSmiliesForList()->fetch();
}
"helper_criteria" 模板#
每当你作为插件开发者希望让目标用户/管理员能够选择用户/页面/其他插件的标准(甚至一次性选择所有标准)时,你可以简单地使用 helper_criteria
。
简而言之,helper_criteria
是一个管理模板,它允许在多个地方使用标准类型的复选框界面,而无需复制粘贴相同的代码。
helper_criteria
包含两种类型的宏:*criteria_name*_tabs
和 *criteria_name*_panes
,用于每种标准类型。例如:用户标准类型的 user_tabs
和 user_panes
宏。
标签#
标签用于在模板中区分不同的标准类型:
使用标签时,第一个标签通常包含与标准无关的字段/选项。然后是标准标签。
在上图中,第一个标签包含通知的选项。红色框中的前两个标签与用户标准类型相关。最后一个与页面标准类型相关。
helper_criteria
中的标签按标准类型宏分组:
<xf:macro name="foo_tabs" arg-container="" arg-active="">
<xf:set var="$tabs">
<a class="tabs-tab{{ $active == 'foo' ? ' is-active' : '' }}"
role="tab" tabindex="0" aria-controls="{{ unique_id('criteriaFoo') }}">Foo 标准</a>
<a class="tabs-tab{{ $active == 'foo_extra' ? ' is-active' : '' }}"
role="tab" tabindex="0" aria-controls="{{ unique_id('criteriaFooExtra') }}">Foo 标准额外</a>
</xf:set>
<xf:if is="$container">
<div class="tabs" role="tablist">
{$tabs|raw}
</div>
<xf:else />
{$tabs|raw}
</xf:if>
</xf:macro>
在上面的代码中,foo
是一个标准类型。它有两个标签,一个用于一般 foo 标准,另一个用于额外的 foo 标准。
面板#
面板只包含标准。
与标签一样,helper_criteria
中的面板按标准类型宏分组:
<xf:macro name="foo_panes" arg-container="" arg-active="" arg-criteria="!" arg-data="!">
<xf:set var="$panes">
<li class="{{ $active == 'foo' ? ' is-active' : '' }}" role="tabpanel" id="{{ unique_id('criteriaFoo') }}">
<xf:checkboxrow label="标准组 1">
<xf:option name="foo_criteria[criterion_1_rule][rule]" value="criterion_1_rule" ... />
<xf:option name="foo_criteria[criterion_2_rule][rule]" value="criterion_2_rule" ... />
</xf:checkboxrow>
<xf:checkboxrow label="标准组 2">
<xf:option name="foo_criteria[criterion_3_rule][rule]" value="criterion_3_rule" ... />
<xf:option name="foo_criteria[criterion_4_rule][rule]" value="criterion_4_rule" ... />
</xf:checkboxrow>
</li>
</xf:set>
<xf:if is="$container">
<ul class="tabPanes">
{$panes|raw}
</ul>
<xf:else />
{$panes|raw}
</xf:if>
</xf:macro>
使用 "helper_criteria"#
要使用 "helper_criteria" 功能,你需要包含它的宏。
准备数据#
如果你没有将选定的标准保存在数据库中的某个地方,或者你想要使用的标准类型不需要任何额外数据,则可以跳过本节。
首先,你需要检索保存的选定标准,并从中创建一个标准对象。在本节中,我们将以页面标准为例:
$savedCriteria = /* 以某种方式检索它... */
// 标准对象
$criteria = $this->app()->criteria('XF:Page', $savedCriteria)->getCriteriaForTemplate();
// 标准额外数据
$criteriaData = $criteria->getExtraTemplateData();
$viewParams = [
/* ... */
'criteria' => $criteria,
'criteriaData' => $criteriaData
];
return $this->view(/* ... */, $viewParams);
包含不带标签的标准#
要包含不带标签的标准,你需要使用 <xf:macro...
标签,并将 arg-container
属性设置为 0
:
<xf:macro template="helper_criteria" name="page_panes" arg-container="0" arg-criteria="{$criteria}" arg-data="{$criteriaData}" />
如果你没有保存的标准,你可以将空数组 {{ [] }}
传递给 arg-criteria
属性。别忘了将 page_panes
中的 page
替换为你想要使用的标准类型的名称。
请注意,所有标准都包裹在 <li>
标签中,因此你需要应用一些 CSS 样式(例如 list-style-type: none;
)。
带标签#
为了使用标准标签,你需要组织页面。遵循以下示例结构:
<xf:form ... class="block">
<div class="block-container">
<!-- 标签 -->
<h2 class="block-tabHeader tabs hScroller" data-xf-init="h-scroller tabs" role="tablist">
<span class="hScroller-scroll">
<!-- 主标签,包含字段/选项 -->
<a class="tabs-tab is-active" role="tab" tabindex="0" aria-controls="MAIN_TAB_ID">主标签标题</a>
<!-- 标准标签 -->
<xf:macro template="helper_criteria" name="page_tabs" arg-userTabTitle="自定义标签名称(可选)" />
</span>
</h2>
<!-- 面板 -->
<ul class="block-body tabPanes">
<!-- 主面板 -->
<li class="is-active" role="tabpanel" id="MAIN_TAB_ID">
<!-- 字段和选项 -->
</li>
<!-- 标准面板 -->
<xf:macro template="helper_criteria" name="page_panes"
arg-criteria="{$criteria}"
arg-data="{$criteriaData}" />
</ul>
<xf:submitrow sticky="true" icon="save" />
</div>
</xf:form>
再次提醒,如果你没有任何保存的标准,或者你甚至不打算保存它,可以将 {{ [] }}
传递给 arg-criteria
属性。
将自定义标准类型添加到 "helper_criteria"#
如果你想将自定义标准类型添加到 helper_criteira
模板中,你需要创建 helper_criteria
模板的模板修改。
转到 ACP 中的“外观 > 模板修改”,切换到“管理员”选项卡,然后点击“添加模板修改”按钮。
我们希望将我们的标签和面板添加到模板的最底部,因此将“搜索类型”切换为“正则表达式”。
在“查找”字段中输入 /$/
。
最后,在“替换”字段中添加标签和面板宏代码。示例:
xf:macro name="foo_tabs" arg-container="" arg-active="">
<xf:set var="$tabs">
<a class="tabs-tab{{ $active == 'foo' ? ' is-active' : '' }}"
role="tab" tabindex="0" aria-controls="{{ unique_id('criteriaFoo') }}">Foo 条件</a>
<a class="tabs-tab{{ $active == 'foo_extra' ? ' is-active' : '' }}"
role="tab" tabindex="0" aria-controls="{{ unique_id('criteriaFooExtra') }}">Foo 额外条件</a>
</xf:set>
<xf:if is="$container">
<div class="tabs" role="tablist">
{$tabs|raw}
</div>
<xf:else />
{$tabs|raw}
</xf:if>
</xf:macro>
<xf:macro name="foo_panes" arg-container="" arg-active="" arg-criteria="!" arg-data="!">
<xf:set var="$panes">
<li class="{{ $active == 'foo' ? ' is-active' : '' }}" role="tabpanel" id="{{ unique_id('criteriaFoo') }}">
<xf:checkboxrow label="条件组 1">
<xf:option name="foo_criteria[criterion_1_rule][rule]" value="criterion_1_rule" ... />
<xf:option name="foo_criteria[criterion_2_rule][rule]" value="criterion_2_rule" ... />
</xf:checkboxrow>
<xf:checkboxrow label="条件组 2">
<xf:option name="foo_criteria[criterion_3_rule][rule]" value="criterion_3_rule" ... />
<xf:option name="foo_criteria[criterion_4_rule][rule]" value="criterion_4_rule" ... />
</xf:checkboxrow>
</li>
</xf:set>
<xf:if is="$container">
<ul class="tabPanes">
{$panes|raw}
</ul>
<xf:else />
{$panes|raw}
</xf:if>
</xf:macro>
现在,您可以在任何地方使用您的条件(参见"使用 helper_criteria")。
自定义用户/页面条件示例#
假设我们想创建一个条件,用于检查我们的用户是否在单条消息上获得了 X 个或更多的赞。
由于我们的条件与用户相关,我们将创建一个属于用户条件的条件。
添加模板修改#
首先,我们需要将我们的条件添加到用户条件列表中。转到 ACP 中的“模板修改”页面,选择“Admin”选项卡,然后点击右上角的“添加模板修改”按钮。
警告
如果没有“Admin”选项卡,请确保您已启用开发模式!
我们将修改 helper_criteria
模板,因此将其写入“模板”字段。在本示例中,我将使用 likes_on_single_message
作为此模板修改的“修改键”。
我们的条件与消息上的赞有关。这意味着它应该位于“内容和成就”部分。这意味着我们只需找到 <!--[XF:user:content_bottom]-->
并将其替换为以下代码:
<xf:option name="user_criteria[likes_on_single][rule]" value="likes_on_single" selected="{$criteria.likes_on_single}" label="单条消息的赞数:">
<xf:numberbox name="user_criteria[likes_on_single][data][likes]" value="{$criteria.likes_on_single.likes}" size="5" min="0" step="1" />
</xf:option>
$0
从这一刻起,我们可以在创建奖杯、通知和用户组晋升时看到甚至设置我们的条件值。
添加代码事件监听器#
我们已经创建了我们的条件。但对 XenForo 来说,它是未知的,当匹配此类条件时,它将始终返回 false
。我们需要告诉 XenForo,当它遇到未知条件时该怎么做。
转到“开发 > 代码事件监听器”页面,然后点击“添加代码事件监听器”按钮。
在“监听事件”字段中选择 criteria_user
(user
因为我们的条件属于用户条件)。在“执行回调”字段中,我们应该指定在匹配条件时要调用的类和方法。
如果您还没有在插件根文件夹中创建文件 Listener.php
,请创建一个,并在其中添加一个新方法 criteriaUser
:
<?php
namespace YOUR_ADDON_ID;
class Listener
{
public static function criteriaUser($rule, array $data, \XF\Entity\User $user, &$returnValue)
{
}
}
您可以将“类”和“方法”字段分别填写为 YOUR_ADDON_ID\Listener
和 criteriaUser
。
处理条件#
由于我们的 criteriaUser
方法会为每个未知条件触发,我们需要确保 $rule
等于 likes_on_single
(我们在 HTML 标记中指定的规则):
public static function criteriaUser($rule, array $data, \XF\Entity\User $user, &$returnValue)
{
switch ($rule)
{
case 'likes_on_single':
/** 处理代码在这里! */
break;
}
}
现在,我们需要编写实际检查用户是否有一条消息获得 X 个或更多赞的代码。
这可以通过简单的 SQL 查询轻松实现,该查询从 xf_post
中选择一条记录,其中赞数(likes
列)大于 X,并且 user_id
等于当前匹配的用户 ID。
所以,查询如下:
方法代码如下:
public static function criteriaUser($rule, array $data, \XF\Entity\User $user, &$returnValue)
{
switch ($rule)
{
case 'likes_on_single':
// 获取数据库
$db = \XF::db();
// 用于选择单个用户帖子最大赞数的数据库查询
$query = "SELECT `likes` FROM `xf_post` WHERE `user_id` = ? ORDER BY `likes` DESC LIMIT 1";
// 检索最大赞数
$likes = $db->fetchOne($query, [$user->user_id]);
// 检查我们是否有数据库结果(我们期望一个数字)
if (is_int($likes)) {
// 如果用户有一条消息获得 X 个或更多赞,则返回 true,否则返回 false
$returnValue = ($likes >= $data['likes']);
} else {
$returnValue = false;
}
break;
}
}
请注意以下几点:
- 我们使用
$user
变量来检索当前匹配的用户。我们可以使用此变量,因为我们的条件属于用户条件。 - 我们可以通过
$data
数组访问数据。它包含来自我们在模板修改中添加的字段的数据。我们只添加了一个<xf:numberbox...
,其name
属性等于user_criteria[likes_on_single][data][likes]
。这就是为什么我们可以在上面的代码中使用$data['likes']
。
现在一切都完成了。让我们测试一下!
测试(奖杯)#
创建一个“All for one”奖杯。在“用户条件”选项卡上,设置“单条消息的赞数”字段,例如 5。
接下来,在您的论坛上创建一个测试消息,然后用五个不同的用户点赞五次(或手动设置 likes
列的值)。
然后,转到“工具 > Cron 条目”并通过点击圆圈箭头按钮运行“更新用户奖杯”cron。
不错!
警告
如果您没有获得“All for one”奖杯,请尝试注销、登录并重新运行“更新用户奖杯”cron。
测试(通知)#
转到“通信 > 通知”并点击“添加通知”按钮。在“用户条件”选项卡上,设置“单条消息的赞数”字段,再次设置为 5。保存通知。
接下来,在您的论坛上创建一个测试消息,然后用五个不同的用户点赞五次(或手动设置 likes
列的值)。
现在,您应该会看到一个通知:
您可以下载基于此示例构建的插件源代码(2.0.10)。
自定义条件类型示例#
想象一下,我们正在创建一个插件(插件 ID:PostsRemover
),用于删除所有符合选定条件的帖子。可用条件列表:
- 帖子至少有 X 个赞
- 帖子作者的用户名为 X
- 帖子至少被编辑了 X 次
- 帖子被编辑的次数不超过 X 次
- 帖子在 X 之前发布
- 帖子在 X 之后发布
显然,对于此类条件,我们需要一个新的条件类型:帖子条件。
条件类型类#
我们应该首先在我们的插件的 Criteria
目录中创建一个继承 AbstractCriteria
的新类 Post
:
<?php
namespace PostsRemover\Criteria;
use XF\Criteria\AbstractCriteria;
class Post extends AbstractCriteria
{
}
现在我们需要为我们插件支持的所有条件编写代码。在本示例中,我将编写上述列表中前三个条件的代码:
<?php
namespace PostsRemover\Criteria;
use XF\Criteria\AbstractCriteria;
class Post extends AbstractCriteria
{
// 帖子至少有 X 个赞
protected function _matchLikeCount(array $data, \XF\Entity\Post $post)
{
return ($post->likes && $post->likes >= $data['likes']);
}
// 帖子作者的用户名为 X
protected function _matchUsername(array $data, \XF\Entity\Post $post)
{
return $post->username === $data['name'];
}
// 帖子至少被编辑了 X 次
protected function _matchEditedCount(array $data, \XF\Entity\Post $post)
{
return $post->edit_count && $post->edit_count >= $data['count'];
}
/* ================ 处理其他条件 ================ */
}
isMatched(...)
方法用于调用我们刚刚创建的 _match
方法,它只接受用户实体,我们需要编写 isMatched()
、isUnknownMatched()
和 isSpecialMatched()
方法的自定义变体。
由于我们正在创建帖子条件,我们需要创建自己的 isMatchedPost()
方法:
public function isMatchedPost(\XF\Entity\Post $post)
{
if (!$this->criteria)
{
return $this->matchOnEmpty;
}
foreach ($this->criteria AS $criterion)
{
$rule = $criterion['rule'];
$data = $criterion['data'];
$specialResult = $this->isSpecialMatchedPost($rule, $data, $post);
if ($specialResult === false)
{
return false;
}
else if ($specialResult === true)
{
continue;
}
$method = '_match' . \XF\Util\Php::camelCase($rule);
if (method_exists($this, $method))
{
$result = $this->$method($data, $post);
if (!$result)
{
return false;
}
}
else
{
if (!$this->isUnknownMatched($rule, $data, $post))
{
return false;
}
}
}
return true;
}
protected function isSpecialMatchedPost($rule, array $data, \XF\Entity\Post $post)
{
return null;
}
protected function isUnknownMatchedPost($rule, array $data, \XF\Entity\Post $post)
{
return false;
}
我们简单地使用了 isMatched(...)
方法代码,将用户实体类型的 $user
变量替换为帖子实体类型的 $post
变量。
由于我们不打算处理特殊和未知条件,我们在 isSpecialMatchedPost
中返回 null,在 isUnknownMathcedPost
中返回 false
。
模板#
将添加管理路由、编写控制器和在幕后执行其他操作的过程放在一边,让我们直接跳到我们页面的模板代码:
<xf:title>帖子删除器</xf:title>
<xf:form action="{{ link('posts-remover/remove') }}" ajax="true" class="block">
<div class="block-container">
<xf:checkboxrow label="帖子条件">
<xf:option label="帖子至少有 X 个赞" name="post_criteria[like_count][rule]" value="like_count">
<xf:numberbox name="post_criteria[like_count][data][likes]" size="5" min="0" step="1" />
</xf:option>
<xf:option label="帖子作者的用户名为 X" name="post_criteria[username][rule]" value="username">
<xf:textbox name="post_criteria[username][data][name]" ac="true" />
</xf:option>
<xf:option label="帖子至少被编辑了 X 次" name="post_criteria[edited_count][rule]" value="edited_count">
<xf:numberbox name="post_criteria[edited_count][data][count]" size="5" min="0" step="1" />
</xf:option>
</xf:checkboxrow>
<!-- 其他条件的模板代码 -->
<xf:submitrow sticky="true" icon="delete"/>
</div>
</xf:form>
匹配条件#
在我们页面的控制器中,我们需要创建一个名为 actionRemove
的方法来处理“删除”按钮点击:
首先,让我们从页面表单中检索 post_criteria
数组:
其次,我们需要从检索到的页面表单数据中创建一个条件对象:
public function actionRemove()
{
$postCriteriaInput = $this->filter('post_criteria', 'array');
/** @var \PostsRemover\Criteria\Post $postCriteria */
$postCriteria = $this->app()->criteria('PostsRemover:Post', $postCriteriaInput);
}
默认情况下,我们的帖子将匹配空条件(当未选择任何内容时),这将导致删除所有论坛帖子。为了避免这种情况,我们需要通过 setMatchOnEmpty()
方法手动设置匹配空条件的结果:
public function actionRemove()
{
$postCriteriaInput = $this->filter('post_criteria', 'array');
/** @var \PostsRemover\Criteria\Post $postCriteria */
$postCriteria = $this->app()->criteria('PostsRemover:Post', $postCriteriaInput);
$postCriteria->setMatchOnEmpty(false); // 如果未选择任何条件,则不会删除任何内容
}
最后,我们需要将所有论坛帖子与选定的条件进行匹配。如果帖子符合条件,我们将删除它:
public function actionRemove()
{
$postCriteriaInput = $this->filter('post_criteria', 'array');
/** @var \PostsRemover\Criteria\Post $postCriteria */
$postCriteria = $this->app()->criteria('PostsRemover:Post', $postCriteriaInput);
$postCriteria->setMatchOnEmpty(false); // 如果未选择任何条件,则不会删除任何内容
// 获取所有论坛帖子
$posts = $this->finder('XF:Post')->fetch();
$deletedCounter = 0;
/** @var \XF\Entity\Post $post */
foreach ($posts as $post)
{
if ($postCriteria->isMatchedPost($post)) // 检查帖子是否符合选定的条件
{
$post->delete(); // 如果帖子符合选定的条件,则删除它
$deletedCounter++;
}
}
return $this->message('完成!删除了 ' . $deletedCounter . ' 条帖子!');
}
注意
请记住,我们在 XenForo 2.1 以下版本中使用 isMatchedPost($post)
方法!
警告
通常,一次性从数据库中检索所有实体(上面代码中的 $this->finder('XF:Post')->fetch();
)是一种不好的做法。可能有数百万条论坛帖子,一次性选择它们将是一个非常漫长的过程,可能会以错误告终。
考虑使用作业系统来处理数十(100+)个数据库项目。
测试#
是时候测试我们的自定义条件类型了!
我在我的测试论坛上创建了三个帖子。第一个帖子获得了 500 个赞,第二个帖子被编辑了 5 次。第三个帖子只是一个普通的、未经处理的帖子,没有赞。
现在,在我们的“帖子删除器”ACP 页面上,让我们选择“帖子至少有 X 个赞”(值为 250)和“帖子至少被编辑了 X 次”(值为 5):
当我点击“删除”按钮时,我看到一条提示消息,告诉我没有删除任何内容。为什么?显然,因为没有帖子同时满足至少有 250 个赞和至少 5 次编辑的条件。
这就是为什么我们需要仅选择第一个条件,然后点击“删除”。这将删除一条有 500 个赞的帖子。接下来,我们需要仅选择最后一个条件并执行删除。有 5 次编辑的帖子将被删除。
结果,只有一条测试帖子在我们的测试中幸存下来:
您可以下载基于此示例构建的插件源代码(2.0.10)。您将在“工具”部分下找到“帖子删除器”ACP 页面。