Yii2 中的活动表单多模型和多属性

active form multi model and multi attributes in Yii2

在我的项目中,我有一个大学模型和系模型之间的聚合:一所大学至少有一个系,而每个系只属于一所大学。 我希望有可能创建一个具有一定数量的部门模型实例的大学模型实例,并且事先不知道部门的确切数量(但至少必须存在一个)。因此,在创建一所大学时,我想要一个包含一个默认部门和一个 "Add Department" 按钮的页面,这样我就可以通过 javascript 添加我需要的任意数量的部门。

问题是:我应该如何使用 ActiveForm 编写创建视图页面,以便我的 POST 数组具有以下结构:

   "University" => ["name" => "Sorbonne", "city" => "Paris"],
   "Faculty" => [
       0 => ["name" => "Medicine", "dean" => "Person A"], 
       1 => ["name" => "Physics", "dean" => "Person B"],
       2 => ["name" => "Mathematics", "dean" => "Person C"],
       ...
   ]

然后我将其传递给 Faculty::loadMultiple() 方法。

我试过类似的东西

   $form = ActiveForm::begin();
   echo $form->field($university, 'name')->textInput();
   echo $form->field($university, 'city')->textInput();
   foreach ($faculties as $i => $faculty) {
       echo $form->field($faculty, "[$i]name")->textInput();
       echo $form->field($faculty, "[$i]dean")->textInput()
   }
   ActiveForm::end();

它有效,但是当通过 javascript 添加新部门时(我只是克隆了一个包含部门输入字段的 html 节点),我不得不详细说明来自变量 $ 的数字我上面的 php 脚本。这很烦人。

我尝试过的另一种可能性是摆脱变量 $i 并编写类似

的内容
    $form = ActiveForm::begin();
    echo $form->field($university, 'name')->textInput();
    echo $form->field($university, 'city')->textInput();
    foreach ($faculties as $faculty) {
        echo $form->field($faculty, "[]name")->textInput();
        echo $form->field($faculty, "[]dean")->textInput()
    }
    ActiveForm::end();

这样克隆对应的节点就很简单了,但是生成的POST数组由于[]括号结构错误

是否可以修改后一种方法并获得所需的 POST 数组结构?

使用 Yii2 动态表单扩展:

安装

安装此扩展的首选方式是通过 composer。

或者运行:

composer require --prefer-dist wbraganca/yii2-dynamicform "dev-master"

或添加到 composer.json 文件的要求部分:

"wbraganca/yii2-dynamicform": "dev-master"

演示页面:Nested Dynamic Form

嵌套动态表单演示源代码:

源代码 - 查看:_form.php

<?php

use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use wbraganca\dynamicform\DynamicFormWidget;

?>

<div class="person-form">

<?php $form = ActiveForm::begin(['id' => 'dynamic-        form']); ?>

 <div class="row">
    <div class="col-sm-6">
        <?= $form->field($modelPerson, 'first_name')->textInput(['maxlength' => true]) ?>
    </div>
    <div class="col-sm-6">
        <?= $form->field($modelPerson, 'last_name')->textInput(['maxlength' => true]) ?>
    </div>
</div>

<div class="padding-v-md">
    <div class="line line-dashed"></div>
</div>

<?php DynamicFormWidget::begin([
    'widgetContainer' => 'dynamicform_wrapper',
    'widgetBody' => '.container-items',
    'widgetItem' => '.house-item',
    'limit' => 10,
    'min' => 1,
    'insertButton' => '.add-house',
    'deleteButton' => '.remove-house',
    'model' => $modelsHouse[0],
    'formId' => 'dynamic-form',
    'formFields' => [
        'description',
    ],
]); ?>
<table class="table table-bordered table-striped">
    <thead>
        <tr>
            <th>Houses</th>
            <th style="width: 450px;">Rooms</th>
            <th class="text-center" style="width: 90px;">
                <button type="button" class="add-house btn btn-success btn-xs"><span class="fa fa-plus"></span></button>
            </th>
        </tr>
    </thead>
    <tbody class="container-items">
    <?php foreach ($modelsHouse as $indexHouse => $modelHouse): ?>
        <tr class="house-item">
            <td class="vcenter">
                <?php
                    // necessary for update action.
                    if (! $modelHouse->isNewRecord) {
                        echo Html::activeHiddenInput($modelHouse, "[{$indexHouse}]id");
                    }
                ?>
                <?= $form->field($modelHouse, "[{$indexHouse}]description")->label(false)->textInput(['maxlength' => true]) ?>
            </td>
            <td>
                <?= $this->render('_form-rooms', [
                    'form' => $form,
                    'indexHouse' => $indexHouse,
                    'modelsRoom' => $modelsRoom[$indexHouse],
                ]) ?>
            </td>
            <td class="text-center vcenter" style="width: 90px; verti">
                <button type="button" class="remove-house btn btn-danger btn-xs"><span class="fa fa-minus"></span></button>
            </td>
        </tr>
     <?php endforeach; ?>
    </tbody>
</table>
<?php DynamicFormWidget::end(); ?>

<div class="form-group">
    <?= Html::submitButton($modelPerson->isNewRecord ? 'Create' : 'Update', ['class' => 'btn btn-primary']) ?>
</div>

<?php ActiveForm::end(); ?>

源代码 - 查看:_form-rooms.php

<?php

use yii\helpers\Html;
use wbraganca\dynamicform\DynamicFormWidget;

?>

<?php DynamicFormWidget::begin([
'widgetContainer' => 'dynamicform_inner',
'widgetBody' => '.container-rooms',
'widgetItem' => '.room-item',
'limit' => 4,
'min' => 1,
'insertButton' => '.add-room',
'deleteButton' => '.remove-room',
'model' => $modelsRoom[0],
'formId' => 'dynamic-form',
'formFields' => [
    'description'
],
]); ?>
<table class="table table-bordered">
<thead>
    <tr>
        <th>Description</th>
        <th class="text-center">
            <button type="button" class="add-room btn btn-success btn-xs"><span class="glyphicon glyphicon-plus"></span></button>
        </th>
    </tr>
</thead>
<tbody class="container-rooms">
<?php foreach ($modelsRoom as $indexRoom => $modelRoom): ?>
    <tr class="room-item">
        <td class="vcenter">
            <?php
                // necessary for update action.
                if (! $modelRoom->isNewRecord) {
                    echo Html::activeHiddenInput($modelRoom, "[{$indexHouse}][{$indexRoom}]id");
                }
            ?>
            <?= $form->field($modelRoom, "[{$indexHouse}][{$indexRoom}]description")->label(false)->textInput(['maxlength' => true]) ?>
        </td>
        <td class="text-center vcenter" style="width: 90px;">
            <button type="button" class="remove-room btn btn-danger btn-xs"><span class="glyphicon glyphicon-minus"></span></button>
        </td>
    </tr>
 <?php endforeach; ?>
</tbody>

源代码 - 控制器

<?php

namespace app\modules\yii2extensions\controllers;

use Yii;
use yii\helpers\ArrayHelper;
use yii\web\NotFoundHttpException;
use yii\web\Response;
use yii\widgets\ActiveForm;
use app\base\Model;
use app\base\Controller;
use app\modules\yii2extensions\models\House;
use app\modules\yii2extensions\models\Person;
use app\modules\yii2extensions\models\Room;
use app\modules\yii2extensions\models\query\PersonQuery;

/**
* DynamicformDemo3Controller implements the CRUD actions for Person model.
*/
class DynamicformDemo3Controller extends Controller
{
/**
 * Lists all Person models.
 * @return mixed
 */
public function actionIndex()
{
    $searchModel = new PersonQuery();
    $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

    return $this->render('index', [
        'searchModel' => $searchModel,
        'dataProvider' => $dataProvider,
    ]);
}

/**
 * Displays a single Person model.
 * @param integer $id
 * @return mixed
 */
public function actionView($id)
{
    $model = $this->findModel($id);
    $houses = $model->houses;

    return $this->render('view', [
        'model' => $model,
        'houses' => $houses,
    ]);
}

/**
 * Creates a new Person model.
 * If creation is successful, the browser will be redirected to the 'view' page.
 * @return mixed
 */
public function actionCreate()
{
    $modelPerson = new Person;
    $modelsHouse = [new House];
    $modelsRoom = [[new Room]];

    if ($modelPerson->load(Yii::$app->request->post())) {

        $modelsHouse = Model::createMultiple(House::classname());
        Model::loadMultiple($modelsHouse, Yii::$app->request->post());

        // validate person and houses models
        $valid = $modelPerson->validate();
        $valid = Model::validateMultiple($modelsHouse) && $valid;

        if (isset($_POST['Room'][0][0])) {
            foreach ($_POST['Room'] as $indexHouse => $rooms) {
                foreach ($rooms as $indexRoom => $room) {
                    $data['Room'] = $room;
                    $modelRoom = new Room;
                    $modelRoom->load($data);
                    $modelsRoom[$indexHouse][$indexRoom] = $modelRoom;
                    $valid = $modelRoom->validate();
                }
            }
        }

        if ($valid) {
            $transaction = Yii::$app->db->beginTransaction();
            try {
                if ($flag = $modelPerson->save(false)) {
                    foreach ($modelsHouse as $indexHouse => $modelHouse) {

                        if ($flag === false) {
                            break;
                        }

                        $modelHouse->person_id = $modelPerson->id;

                        if (!($flag = $modelHouse->save(false))) {
                            break;
                        }

                        if (isset($modelsRoom[$indexHouse]) && is_array($modelsRoom[$indexHouse])) {
                            foreach ($modelsRoom[$indexHouse] as $indexRoom => $modelRoom) {
                                $modelRoom->house_id = $modelHouse->id;
                                if (!($flag = $modelRoom->save(false))) {
                                    break;
                                }
                            }
                        }
                    }
                }

                if ($flag) {
                    $transaction->commit();
                    return $this->redirect(['view', 'id' => $modelPerson->id]);
                } else {
                    $transaction->rollBack();
                }
            } catch (Exception $e) {
                $transaction->rollBack();
            }
        }
    }

    return $this->render('create', [
        'modelPerson' => $modelPerson,
        'modelsHouse' => (empty($modelsHouse)) ? [new House] : $modelsHouse,
        'modelsRoom' => (empty($modelsRoom)) ? [[new Room]] : $modelsRoom,
    ]);
}

/**
 * Updates an existing Person model.
 * If update is successful, the browser will be redirected to the 'view' page.
 * @param integer $id
 * @return mixed
 */
public function actionUpdate($id)
{
    $modelPerson = $this->findModel($id);
    $modelsHouse = $modelPerson->houses;
    $modelsRoom = [];
    $oldRooms = [];

    if (!empty($modelsHouse)) {
        foreach ($modelsHouse as $indexHouse => $modelHouse) {
            $rooms = $modelHouse->rooms;
            $modelsRoom[$indexHouse] = $rooms;
            $oldRooms = ArrayHelper::merge(ArrayHelper::index($rooms, 'id'), $oldRooms);
        }
    }

    if ($modelPerson->load(Yii::$app->request->post())) {

        // reset
        $modelsRoom = [];

        $oldHouseIDs = ArrayHelper::map($modelsHouse, 'id', 'id');
        $modelsHouse = Model::createMultiple(House::classname(), $modelsHouse);
        Model::loadMultiple($modelsHouse, Yii::$app->request->post());
        $deletedHouseIDs = array_diff($oldHouseIDs, array_filter(ArrayHelper::map($modelsHouse, 'id', 'id')));

        // validate person and houses models
        $valid = $modelPerson->validate();
        $valid = Model::validateMultiple($modelsHouse) && $valid;

        $roomsIDs = [];
        if (isset($_POST['Room'][0][0])) {
            foreach ($_POST['Room'] as $indexHouse => $rooms) {
                $roomsIDs = ArrayHelper::merge($roomsIDs, array_filter(ArrayHelper::getColumn($rooms, 'id')));
                foreach ($rooms as $indexRoom => $room) {
                    $data['Room'] = $room;
                    $modelRoom = (isset($room['id']) && isset($oldRooms[$room['id']])) ? $oldRooms[$room['id']] : new Room;
                    $modelRoom->load($data);
                    $modelsRoom[$indexHouse][$indexRoom] = $modelRoom;
                    $valid = $modelRoom->validate();
                }
            }
        }

        $oldRoomsIDs = ArrayHelper::getColumn($oldRooms, 'id');
        $deletedRoomsIDs = array_diff($oldRoomsIDs, $roomsIDs);

        if ($valid) {
            $transaction = Yii::$app->db->beginTransaction();
            try {
                if ($flag = $modelPerson->save(false)) {

                    if (! empty($deletedRoomsIDs)) {
                        Room::deleteAll(['id' => $deletedRoomsIDs]);
                    }

                    if (! empty($deletedHouseIDs)) {
                        House::deleteAll(['id' => $deletedHouseIDs]);
                    }

                    foreach ($modelsHouse as $indexHouse => $modelHouse) {

                        if ($flag === false) {
                            break;
                        }

                        $modelHouse->person_id = $modelPerson->id;

                        if (!($flag = $modelHouse->save(false))) {
                            break;
                        }

                        if (isset($modelsRoom[$indexHouse]) && is_array($modelsRoom[$indexHouse])) {
                            foreach ($modelsRoom[$indexHouse] as $indexRoom => $modelRoom) {
                                $modelRoom->house_id = $modelHouse->id;
                                if (!($flag = $modelRoom->save(false))) {
                                    break;
                                }
                            }
                        }
                    }
                }

                if ($flag) {
                    $transaction->commit();
                    return $this->redirect(['view', 'id' => $modelPerson->id]);
                } else {
                    $transaction->rollBack();
                }
            } catch (Exception $e) {
                $transaction->rollBack();
            }
        }
    }

    return $this->render('update', [
        'modelPerson' => $modelPerson,
        'modelsHouse' => (empty($modelsHouse)) ? [new House] : $modelsHouse,
        'modelsRoom' => (empty($modelsRoom)) ? [[new Room]] : $modelsRoom
    ]);
}

/**
 * Deletes an existing Person model.
 * If deletion is successful, the browser will be redirected to the 'index' page.
 * @param integer $id
 * @return mixed
 */
public function actionDelete($id)
{
    $model = $this->findModel($id);
    $name = $model->first_name;

    if ($model->delete()) {
        Yii::$app->session->setFlash('success', 'Record  <strong>"' . $name . '"</strong> deleted successfully.');
    }

    return $this->redirect(['index']);
}

/**
 * Finds the Person model based on its primary key value.
 * If the model is not found, a 404 HTTP exception will be thrown.
 * @param integer $id
 * @return Person the loaded model
 * @throws NotFoundHttpException if the model cannot be found
 */
protected function findModel($id)
{
    if (($model = Person::findOne($id)) !== null) {
        return $model;
    } else {
        throw new NotFoundHttpException('The requested page does not exist.');
    }
}
}