yii2yii2-validation

Custom validation on DynamicFormWidget in Yii2 not working


I am trying to add a custom validation for a DynamicFormWidget widget in Yii2. There is a DynamicFormWidget widget 'percentage' and the sum total of all 'percentage' values dynamically created should be 100. I added a validation rule

public function checkPercentageTotal($attribute, $params){
        foreach (Yii::$app->request->post()['Category'] as $percentage){
        $percentage_values[]=$percentage['percentage'];
        }
        if( array_sum($percentage_values)<>100){
              $this->addError($attribute, 'Total percentage should be 100');return false;
        }
    }

and added

public function rules()
    {
        return [

            [['percentage'] ,'checkPercentageTotal'],
        ];
    }

But is not showing error.The code is executing inside function checkPercentageTotal()

This is the code in my view file:

<?php

use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use wbraganca\dynamicform\DynamicFormWidget;
use kartik\date\DatePicker;
use kartik\select2\Select2;
use dosamigos\tinymce\TinyMce;
use yii\helpers\ArrayHelper;
use backend\models\Department;
$script = <<< JS
$(".dynamicform_wrapper").on("afterInsert", function(e, item) {
    console.log("afterInsert");

      var row = $(this).closest('td');
       var row=jQuery(item).find('select:eq(1)');
       row.val('5')

});

JS;
$this->registerJs($script);
?>
<br>
<div class="dynamicform_wrapper">

<?php $form = ActiveForm::begin(['id' => 'dynamic-form', ]); ?>
    <div class="box box-primary box-solid">
        <div class="box-header with-border">
            <h3 class="box-title">Create Survey Form</h3>

        </div><br>
<div class="col-sm-12">
<div class="row">
    <div class="col-sm-2">
        <?= $form->field($modelSurvey, 'form_id')->textInput(['maxlength' => true]) ?>
    </div>
    <div class="col-sm-6">
        <?= $form->field($modelSurvey, 'title')->textInput(['maxlength' => true]) ?>
    </div>
    <div class="col-sm-2">
    <?php

    $data=ArrayHelper::map(Department::find()->all(), 'id', 'department');
    echo '<label class="control-label">Department</label>';
    echo Select2::widget([
        'name' => 'depart',
        'id' => 'department',
        'theme' =>Select2::THEME_BOOTSTRAP,
        'value' => isset($modelSurvey->depart) ? $modelSurvey->depart : [],
        'data' => $data,
        // 'initValueText' => isset($model->dept) ? $model->dept : [],
        'options' =>  [
            'placeholder' => Yii::t('app', 'Choose Department...'),
            'multiple' => true,
        ],
        'pluginOptions' => [
            'tags' => true,
            'allowClear' => true,
            'width' => '100%'
        ],]);

    ?>
    </div>
    <div class="col-sm-2">
        <?php

       $data=array('1'=>'Staff','2'=>'Student');
        echo '<label class="control-label">Type</label>';
        echo Select2::widget([
            'name' => 'type_id',
            'id' => 'type_id',
            'theme' =>Select2::THEME_BOOTSTRAP,
            'value' => isset($modelSurvey->type_id) ? $modelSurvey->type_id : [],
            'data' => $data,
            // 'initValueText' => isset($model->dept) ? $model->dept : [],
            'options' =>  [
                'placeholder' => Yii::t('app', 'Choose Type...'),
               // 'multiple' => true,
            ],
            'pluginOptions' => [
                'tags' => true,
                'allowClear' => true,
                'width' => '100%'
            ],]);
        ?>
    </div>
</div>
    <div class="row">

        <div class="col-sm-6">
            <?= $form->field($modelSurvey, 'abstract')->widget(TinyMce::className(), [
                'options' => ['rows' => 6],
                'language' => 'en',
                'clientOptions' => [
                    'plugins' => [
                        "advlist autolink lists link charmap print preview anchor",
                        "searchreplace visualblocks code fullscreen",
                        "insertdatetime media table contextmenu paste"
                    ],
                    'toolbar' => "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image"
                ]
            ]); ?>
        </div>
    <div class="col-sm-6">
        <?= $form->field($modelSurvey, 'ledger')->widget(TinyMce::className(), [
            'options' => ['rows' => 6],
            'language' => 'en',
            'clientOptions' => [
                'plugins' => [
                    "advlist autolink lists link charmap print preview anchor",
                    "searchreplace visualblocks code fullscreen",
                    "insertdatetime media table contextmenu paste"
                ],
                'toolbar' => "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image"
            ]
        ]); ?>

    </div>
    </div>
</div>

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



    <table class="table table-bordered table-striped">

    <tbody class="container-items">
    <?php foreach ($modelsCategory as $indexCategory => $modelCategory): ?>
        <tr class="house-item" >

                <?php
                // necessary for update action.
                if (! $modelCategory->isNewRecord) {
                    echo Html::activeHiddenInput($modelCategory, "[{$indexCategory}]id");
                }
                ?>
            <th colspan="5" bgcolor="#EC7063" style="display: inline-grid; width: 100%;">Categories</th>

            <td style="display: inline-grid; width: 8%;"><?= $form->field($modelCategory, "[{$indexCategory}]percentage")->label(true)->textInput(['maxlength' => true]) ?></td>

 <td style="display: inline-block; width: 10%;">
                <button type="button" class="add-house btn btn-success btn-xs"><span class="glyphicon glyphicon-plus"></span></button>
                <button type="button" class="remove-house btn btn-danger btn-xs"><span class="glyphicon glyphicon-minus"></span></button>
            </td>


            <td style="display: block;" >

                <?= $this->render('_form-rooms', [
                    'form' => $form,
                    'indexCategory' => $indexCategory,
                    'modelsQuestion' => $modelsQuestion[$indexCategory],
                ]) ?>

            </td>
    </tr>



    <?php endforeach; ?>
    </tbody>
</table>
<?php DynamicFormWidget::end(); ?>

<div class="form-group">
    <div style="padding-left: 1%;">
    <?= Html::submitButton($modelSurvey->isNewRecord ? 'Create' : 'Update', ['class' => 'btn btn-lg btn-danger']) ?>
</div></div>

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

The code in CategoryModel is

<?php

namespace backend\models;

use Yii;

/**
 * This is the model class for table "category".
 *
 * @property int $id
 * @property string $survey_id

 */
class Category extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'category';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['survey_id', 'percentage'], 'required'],
            [['survey_id' ], 'string', 'max' => 250],
            [['percentage'] ,'checkPercentageTotal'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('app', 'ID'),
            'survey_id' => Yii::t('app', 'Survey ID'),

            'percentage' => Yii::t('app', 'Percent'),
        ];
    }
    public function getQuestions()
    {
        return $this->hasMany(Question::className(), ['cat_id' => 'id']);
    }

    public function checkPercentageTotal($attribute, $params){
        foreach (Yii::$app->request->post()['Category'] as $percentage){
        $percentage_values[]=$percentage['percentage'];
        }
        if( array_sum($percentage_values)<>100){
              $this->addError($attribute, 'Total percentage should be 100');return false;
        }
    }
}

and code of SurveyModel is

<?php

namespace backend\models;

use Yii;

/**
 * This is the model class for table "survey".
 *
 * @property int $id
 * @property string $form_id
 * @property string $dept
 * @property string $title
 * @property string $period
 * @property string $abstract
 * @property string $ledger
 * @property string $apr_name
 * @property string $apr_desg
 * @property string $sem
 * @property string $acad_yr
 * @property string $sec
 * @property string $qualification
 */
class Survey extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */

    /**===============================
     * floated variable for survey form
     */

      /**
       * end here =====================
       */

    public static function tableName()
    {
        return 'survey';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['form_id', 'title', 'abstract', 'ledger','date_created'], 'required'],
            [['form_id'], 'string', 'max' => 1250],


        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('app', 'ID'),
            'form_id' => Yii::t('app', 'Form ID'),
            'title' => Yii::t('app', 'Title'),
            'abstract' => Yii::t('app', 'Abstract'),
            'ledger' => Yii::t('app', 'Ledger'),
            'date_created' =>Yii::t('app', 'Date Created'),
            'depart' =>Yii::t('app', 'Department'),


        ];
    }


}

Code of create actiion controller is

public function actionCreate()
    {
        $modelSurvey = new Survey;
        $modelsCategory = [new Category];
        $modelsQuestion = [[new Question]];

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

            foreach($_POST['depart'] as $departindex => $departvalue){
                    $department[]=$departvalue;

            }
            $dep=implode(',',$department);

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

            // validate survey and category models
            $valid[] = $modelSurvey->validate();


            $valid[] = Model::validateMultiple($modelsCategory) && $valid;

            if (isset($_POST['Question'][0][0])) {
                foreach ($_POST['Question'] as $indexCategory => $questions) {
                    foreach ($questions as $indexQuestion => $question) {
                        $data['Question'] = $question;
                        $modelQuestion = new Question;
                        $modelQuestion->load($data);
                        $modelsQuestion[$indexCategory][$indexQuestion] = $modelQuestion;
                        $valid[] = $modelQuestion->validate();
                    }
                }
            }

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

                    $today = date("Y-m-d H:i:s");
                    $modelSurvey->date_created=$today;
                    $modelSurvey->depart=$dep;
                    $modelSurvey->type_id=$_POST['type_id'];
                    if ($flag = $modelSurvey->save(false)) {
                        foreach ($modelsCategory as $indexCategory => $modelCategory) {

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

                            $modelCategory->survey_id = $modelSurvey->id;

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

                            if (isset($modelsQuestion[$indexCategory]) && is_array($modelsQuestion[$indexCategory])) {
                                foreach ($modelsQuestion[$indexCategory] as $indexQuestion => $modelQuestion) {
                                    $modelQuestion->cat_id = $modelCategory->id;
                                    if (!($flag = $modelQuestion->save(false))) {
                                        break;
                                    }
                                }
                            }
                        }
                    }

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

        return $this->render('create', [
            'modelSurvey' => $modelSurvey,
            'modelsCategory' => (empty($modelsCategory)) ? [new Category] : $modelsCategory,
            'modelsQuestion' => (empty($modelsQuestion)) ? [[new Question]] : $modelsQuestion,
        ]);
    }

I think custom validation rules do not have any effect on Dynamic Form widget elements


Solution

  • I guess this is the main problem:

    $valid[] = $modelSurvey->validate();
    $valid[] = Model::validateMultiple($modelsCategory) && $valid;
    
    // ...
    
    if ($valid) {
    

    Even if all your validations returns false, you will get array of falses, which will be treated as true (non-empty array === true). You may try to change this code with something like:

    $valid = Model::validateMultiple($modelsCategory) && $valid;
    

    But honestly, your code is really messy and requires some serious refactoring. You should not access GET or POST data directly inside of model, and this action and validation is extremely overcomplicated.

    For such cases you should create separated SurveyForm and all validation and saving should be inside of it. checkPercentageTotal validator does not really fit to Category model - right now you're doing the same validation multiple times (for every category separately). This validation probably should be done at SurveyForm model, only once.