Yii: Динамическое изменение правил валидации (сценариев)

в 20:06, , рубрики: cactiverecord, php, yii, валидация, метки: , ,

В данном маленьком топике я хотел бы рассказать об одном очень простом рецепте (который многим из вас, вероятно знаком) в контексте фреймворка Yii. Речь идет о динамическом изменении правил валидации формы — когда правила валидации изменяются в зависимости от выбора пользователя вашего приложения, сделанного, например, путем выбора значения из списка или установленного чекбокса.

На применение данного решения меня натолкнула практическая необходимость при написании одного простенького Контакт-Центра с веб-интерфейсом. Чтоб не придумывать примеры, вкратце поясню применение данного рецепта на примере этого же КЦ. Как известно, основная деятельность операторов КЦ заключается в приеме звонков от клиентов и в исходящих звонках клиентам. По результатам звонка оператор должен занести в систему определенные данные — результат разговора с клиентом, на основании которого определяется статус тикета и дальнейшая логика работы с ним (назначение повторного звонка, любой другой процедуры, закрытие тикета и т.д.). Проблема заключается в том, что некоторые поля формы (читай — атрибуты модели) должны быть заполнены или незаполнены в зависимости от «родительского» статуса. Например, очевидно, что поле «Результат разговора с клиентом» не имеет смысла заполнять, если технический статус звонка — «Недозвон/Неверный номер» ( технический статус должен выставлять оператор, а не система, поскольку звонок может состояться, но получателем звонка ВДРУГ может оказаться не клиент — ошибка в номере и т.д. ). В то же время, если выставлен технический статус «Успешный дозвон» — поле «Результат разговора с клиентом» обязательно должно быть заполнено. Снова-таки: в зависимости от результата разговора может появиться необходимость в заполнении дополнительных полей — например поля «Дата повторного звонка» при статусе «Повторный звонок».
Естественно, я хотел бы, чтобы в зависимости от значения определенного поля формы менялись и правила валидации для всей формы. На практике эта задача очень проста в применении, но очень полезна в таких случаях, как мой.
Итак:
1. Создаем несколько сценариев для вадидации формы звонка.
Сначала создадим правила для тех полей, которые необходимы во всех сценариях:

public function rules(){
    //...
   array('call_status', 'required', 'on'=>'callSubmit, invalidNumberSubmit, validCallSubmit, successCallSubmit...' //...
     'message'=>'Поле "{attribute}" не может быть пустым.'),
}

Думаю первое правило не вызывает вопросов — технический статус звонка — это значение верхнего уровня в форме — оно необходимо в любом сценарии.
Продолжим идти от общего к частному:

//.. Inside rules() method
array('talk_status',  'required', 'on'=>'successCallSubmit, secondCall,...') /* здесь список сценариев, связанных с успешным дозвоном и статусами разговора */

Далее, определяем правила для более частных случаев:

//.. Inside rules() method
array('new_call_date',  'required',	'on'=>'secondCall') /*дата повторного звнока при статусе "Повторный звонок"*/

Таким образом мы можем перебрать все необходимые правила валидации — как в общих случаях, так и в каждом частном. Дело остается за малым: сделать так, чтобы правила валидации менялись в зависимости от выбранных значений в форме. Как вы уже наверное догадались, для этого мы будем использовать CActiveForm, так как этот виджет из коробки позволяет просто и ясно проводить ajax-валидацию.
Создадим для примера простенькую форму:

	<?php $form=$this->beginWidget('CActiveForm', array(
			'id'=>'call-submit-form',			
			'enableAjaxValidation'=>true,			
			'clientOptions'=>array(
					'validateOnChange'=>true,
					'validateOnSubmit'=>true
					),
                       //всяческие настройки виджета
)); ?>

Из интересных нам опций необходимо отметить такие: 'enableAjaxValidation', 'validateOnChange','validateOnSubmit'. Полагаю, предназначение этих опций не вызывает вопросов и понятно из их названий. Продолжим:

<div class='control-group'>	
	<?php echo $form->labelEx($model,'call_status'); ?>
	<?php echo $form->dropDownList($model,'call_status',
     CHtml::listData( CallStatuses::model()->findAll(),'id','title' ) ); ?>
			
	<?php echo $form->labelEx($model,'talk_status'); ?>
	<?php echo $form->dropDownList($model,'talk_status',  
     CHtml::listData(TalkStatuses::model()->findAll(),'id','title' ), )); ?>

       <?php echo $form->error($model,'call_status'); ?>
       <?php echo $form->error($model,'talk_status'); ?>
</div>

<div class='control-group'>
<?php echo $form->label($model,'new_call_date'); ?>
		<?php  //Поле выбора даты повторного звонка
           $this->widget('zii.widgets.jui.CJuiDatePicker', array(
				'attribute'=>'new_call_date',				
				'model'=>$model,
				//...
                ),               
		));		
		?>
</div>

Не буду утомлять вас большим количеством полей, думаю трех и так достаточно для примера. Все, что нам осталось сделать, — это собственно реализовать функционал для динамического изменения правил валидации (читай — сценария).
На самом деле, в основе этого функционала лежит простейшая логика: нужно каждому значению поля сопоставить сценарий, по которому будет валидироваться форма после выбора этого значения (в данном случае при выборе значения из списка, созданного методом CActiveForm::dropDownList() ). Это очень простое решение, и я не стану зацикливаться на деталях и создавать реализацию в ООП-манере, ведь главное донести идею.
Вместо этого я для простоты помещу соответствующий код прямо в метод performAjaxValidation() класса контроллера. Ниже немного измененный код стандартного метода performAjaxValidation() и метода, который принимает форму:

//Inside controller 
public function actionSaveCall(){
/*Изначально создаем модель со сценарием 'callSubmit', который считает необходимым всего одно поле - 'call_status'  - технический статус звонка. */
		$model=new Calls('callSubmit');
		$this->performCallAjaxValidation($model);
		if( isset($_POST['Calls']) ) {
			$model->attributes = $_POST['Calls'];			
			if( $model->save() )
				$this->redirect( Yii::app()->user->returnUrl );			
		}					
	}

protected function performAjaxValidation($model)
	{
		if(isset($_POST['ajax']) && $_POST['ajax']==='calls-form')
		{
          $callStatusesScenarios = array( Calls::CALL_FAIL=>'validCallSubmit', Calls::SUCCESS_CALL=>'successCallSubmit', Calls::WRONG_NUMBER=>'invalidNumberSubmit');

if( !empty($_POST['Calls']['call_status']) && !empty( $callStatusesScenarios[ $_POST['Calls']['call_status'] ] ) ){
				$model->setScenario ( $callStatusesScenarios[ $_POST['Calls']['call_status'] ] );
			}
			echo CActiveForm::validate($model);
			Yii::app()->end();
		}
	}	

Как вы видите из кода, все, что я делаю — это просто меняю сценарий в зависимости от пришедшего значения поля 'call_status'. Теперь, если оператор выберет в форме статус дозвона «Успешный дозвон», сценарий модели при валидации изменится на 'successCallSubmit', который ожидает от формы теперь еще и статус разговора — в результате, выбрав «Успешный дозвон», оператор уже не сможет засабмитить форму, пока не заполнит поле «Результат разговора». В то же время, если статус дозвона будет «Неправильный номер», форма засабмитится без всяких дополнительных полей. По тому же принципу можно действовать и далее — со статусами разговоров и их дополнительными полями и т.д.
Применение такого подхода будет полезным в таких ситуациях, когда количество и виды необходимых для заполнения полей будут меняться в зависимости от выбора пользователя. Прошу прощения за немного каламбурное изложение, и успехов вам в разработке на замечательном фреймворке Yii.

Автор: LayneBuchyn

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js