無限級分類管理中,編輯分類時必須排除「自己」與「自己的子孫分類」作為父分類選項,否則會造成邏輯循環。本文展示 Yii 2.0 如何在表單中動態處理此業務規則。
CREATE TABLE category (
id INT PRIMARY KEY AUTO_INCREMENT,
pid INT DEFAULT 0 COMMENT '父分類 ID,0 為頂層',
title VARCHAR(100) NOT NULL COMMENT '分類標題',
name VARCHAR(100) NOT NULL COMMENT '分類別名',
keywords VARCHAR(255) COMMENT 'SEO 關鍵字',
description VARCHAR(255) COMMENT '分類描述',
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1
);
<?php
use common\models\Category;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model common\models\Category */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="category-form">
<div class="row">
<?php
// 關鍵邏輯:編輯時排除自己
if (!$model->isNewRecord) {
$cate = ArrayHelper::map(
Category::find()
->andWhere('id != :id', [':id' => $model->id])
->all(),
'id', 'title'
);
} else {
$cate = ArrayHelper::map(Category::find()->all(), 'id', 'title');
}
?>
<div class="col-md-6 col-md-offset-3">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'title')->textInput(['maxlength' => 100])->label('分類標題') ?>
<?= $form->field($model, 'name')->textInput(['maxlength' => 100])->label('分類別名') ?>
<?= $form->field($model, 'pid')->dropDownList($cate, [
'prompt' => '請選擇父分類(留空為頂層)',
])->label('父分類') ?>
<?= $form->field($model, 'keywords')->textarea(['maxlength' => 255])->label('分類關鍵詞') ?>
<?= $form->field($model, 'description')->textarea(['maxlength' => 255])->label('分類描述') ?>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<?= Html::submitButton(
$model->isNewRecord ? '建立' : '更新',
['class' => $model->isNewRecord ? 'btn btn-block btn-success' : 'btn btn-block btn-primary']
) ?>
</div>
</div>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
若業務規則要求「不能選自己及所有子孫分類」,需實作遞迴查詢:
// Category 模型加入方法
public function getDescendantIds()
{
$ids = [$this->id];
$children = self::find()->where(['pid' => $this->id])->all();
foreach ($children as $child) {
$ids = array_merge($ids, $child->getDescendantIds());
}
return $ids;
}
// 視圖中使用
if (!$model->isNewRecord) {
$excludeIds = $model->getDescendantIds();
$cate = ArrayHelper::map(
Category::find()
->andWhere(['not in', 'id', $excludeIds])
->all(),
'id', 'title'
);
}
// 使用 WITH RECURSIVE 一次查詢取得所有子孫 ID
$sql = "
WITH RECURSIVE descendants AS (
SELECT id FROM category WHERE id = :id
UNION ALL
SELECT c.id FROM category c
INNER JOIN descendants d ON c.pid = d.id
)
SELECT id FROM descendants
";
$excludeIds = Yii::$app->db->createCommand($sql, [':id' => $model->id])->queryColumn();
$cate = ArrayHelper::map(
Category::find()->andWhere(['not in', 'id', $excludeIds])->all(),
'id', 'title'
);
對於大量分類,建議使用 jsTree 替代原生下拉選單:
// 視圖
<div id="category-tree"></div>
<input type="hidden" name="Category[pid]" id="category-pid">
<?php
$this->registerJs('
$("#category-tree").jstree({
"core": {
"data": ' . json_encode($treeData) . ',
"themes": {"icons": false}
},
"checkbox": {"three_state": false}
}).on("select_node.jstree", function(e, data) {
$("#category-pid").val(data.node.id === "#" ? "" : data.node.id);
});
');
?>
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post())) {
// 伺服器端二次驗證
if ($model->pid == $model->id) {
$model->addError('pid', '父分類不能是自己');
return $this->render('update', ['model' => $model]);
}
// 檢查是否選了自己的子孫
if (in_array($model->pid, $model->getDescendantIds())) {
$model->addError('pid', '父分類不能是自己的子分類');
return $this->render('update', ['model' => $model]);
}
if ($model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
}
return $this->render('update', ['model' => $model]);
}
分類管理看似簡單,實則包含遞迴邏輯、前後端驗證、使用者體驗等多層面考量。Yii 2.0 的 ActiveForm、ArrayHelper、模型驗證機制完美支援此類業務場景。