Использование конструктора форм
===============================

При создании HTML форм часто приходится писать довольно большое количество повторяющегося кода,
который почти невозможно использовать в других проектах. К примеру, для каждого поля ввода
нам необходимо вывести описание и возможные ошибки валидации. Для того чтобы сделать
возможным повторное использование подобного кода, можно использовать конструктор форм.


Общая идея
----------

Конструктор форм использует объект [CForm] для описания параметров, необходимых для
создания HTML формы, таких как модели и поля, используемые в форме, а также параметры
построения самой формы. Разработчику достаточно создать объект [CForm], задать его параметры
и вызвать метод для построения формы.

Параметры формы организованы в виде иерархии элементов формы. Корнем является
объект [CForm]. Корневой объект формы включает в себя две коллекции, содержащие
другие элементы: [CForm::buttons] и [CForm::elements]. Первая содержит кнопки
(такие как «Сохранить» или «Очистить»), вторая — поля ввода, статический текст и
вложенные формы — объекты [CForm], находящиеся в коллекции [CForm::elements]
другой формы. Вложенная форма может иметь свою модель данных и коллекции
[CForm::buttons] и [CForm::elements].

Когда пользователи отправляют форму, данные, введённые в поля ввода всей иерархии формы, включая
вложенные формы, передаются на сервер. [CForm] включает в себя методы, позволяющие
автоматически присвоить данные полям соответствующей модели и провести валидацию.


Создание простой формы
----------------------

Ниже будет показано, как построить форму входа на сайт.

Сначала реализуем действие `login`:

~~~
[php]
public function actionLogin()
{
	$model = new LoginForm;
	$form = new CForm('application.views.site.loginForm', $model);
	if($form->submitted('login') && $form->validate())
		$this->redirect(array('site/index'));
	else
		$this->render('login', array('form'=>$form));
}
~~~

Вкратце, здесь мы создали объект [CForm], используя конфигурацию, найденную
по пути, который задан псевдонимом `application.views.site.loginForm`.
Объект [CForm], как описано в разделе «[Создание модели](/doc/guide/form.model)»,
использует модель `LoginForm`.

Если форма отправлена, и все входные данные прошли проверку без ошибок,
перенаправляем пользователя на страницу `site/index`. Иначе выводим представление
`login`, описывающее форму.

Псевдоним пути `application.views.site.loginForm` указывает на файл PHP
`protected/views/site/loginForm.php`. Этот файл возвращает массив, описывающий
настройки, необходимые для [CForm]:

~~~
[php]
return array(
	'title'=>'Пожалуйста, представьтесь',

    'elements'=>array(
        'username'=>array(
            'type'=>'text',
            'maxlength'=>32,
        ),
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),
        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),

    'buttons'=>array(
        'login'=>array(
            'type'=>'submit',
            'label'=>'Вход',
        ),
    ),
);
~~~

Настройки, приведённые выше, являются ассоциативным массивом, состоящим из пар имя-значение,
используемых для инициализации соответствующих свойств [CForm]. Самыми важными
свойствами, как мы уже упомянули, являются [CForm::elements] и [CForm::buttons].
Каждое из них содержит массив, определяющий элементы формы. Более детальное описание
элементов формы будет приведено в следующем подразделе.

Опишем шаблон представления `login`:

~~~
[php]
<h1>Вход</h1>

<div class="form">
<?php echo $form; ?>
</div>
~~~

> Tip|Подсказка: Приведённый выше код `echo $form;` эквивалентен `echo $form->render();`.
> Использование более компактной записи возможно, так как [CForm] реализует магический
> метод `__toString`, в котором вызывается метод `render()`, возвращающий
> код формы.


Описание элементов формы
------------------------

При использовании конструктора форм вместо написания разметки мы, главным образом,
описываем элементы формы. В данном подразделе мы опишем, как задать свойство [CForm::elements].
Мы не будем описывать [CForm::buttons], так как конфигурация этого свойства практически
ничем не отличается от [CForm::elements].

Свойство [CForm::elements] является массивом, каждый элемент которого соответствует
элементу формы. Это может быть поле ввода, статический текст или вложенная форма.

### Описание поля ввода

Поле ввода, главным образом, состоит из заголовка, самого поля, подсказки и текста ошибки и
должно соответствовать определённому атрибуту модели. Описание поля ввода содержится в
экземпляре класса [CFormInputElement]. Приведённый ниже код массива [CForm::elements]
описывает одно поле ввода:

~~~
[php]
'username'=>array(
    'type'=>'text',
    'maxlength'=>32,
),
~~~

Здесь указано, что атрибут модели называется `username`, тип поля — `text` и его атрибут
`maxlength` равен 32.

Любое доступное для записи свойство [CFormInputElement] может быть настроено приведённым выше
способом. К примеру, можно задать свойство [hint|CFormInputElement::hint] для того, чтобы
показывать подсказку или свойство [items|CFormInputElement::items], если
поле является выпадающим списком или группой элементов checkbox или radio.
Если имя опции не является свойством [CFormInputElement], оно будет считаться атрибутом
соответствующего HTML-тега input. Например, так как опция `maxlength` не является
свойством [CFormInputElement], она будет использована как атрибут `maxlength` HTML-элемента
input.

Следует отдельно остановиться на свойстве [type|CFormInputElement::type].
Оно определяет тип поля ввода. К примеру, тип `text` означает, что будет использован
элемент формы `input`, а `password` — поле для ввода пароля. В [CFormInputElement]
реализованы следующие типы полей ввода:

 - text
 - hidden
 - password
 - textarea
 - file
 - radio
 - checkbox
 - listbox
 - dropdownlist
 - checkboxlist
 - radiolist

Отдельно следует описать использование "списочных" типов `dropdownlist`, `checkboxlist`
и `radiolist`. Для них необходимо задать свойство [items|CFormInputElement::items]
соответствующего элемента input. Сделать это можно так:

~~~
[php]
'gender'=>array(
    'type'=>'dropdownlist',
    'items'=>User::model()->getGenderOptions(),
    'prompt'=>'Выберите значение:',
),

…

class User extends CActiveRecord
{
	public function getGenderOptions()
	{
		return array(
			0 => 'Мужчина',
			1 => 'Женщина',
		);
	}
}
~~~

Данный код сгенерирует выпадающий список с текстом «Выберите значение:» и опциями
«Мужчина» и «Женщина», которые мы получаем из метода `getGenderOptions` модели `User`.

Кроме данных типов полей, в свойстве [type|CFormInputElement::type] можно указать
класс или псевдоним пути виджета. Класс виджета должен наследовать [CInputWidget] или [CJuiInputWidget].
В ходе генерации элемента формы будет создан и выполнен экземпляр класса виджета.
Виджет будет использовать конфигурацию, переданную через настройки элемента формы.


### Описание статического текста

Довольно часто в форме, помимо полей ввода, содержится некоторая декоративная
HTML разметка. К примеру, горизонтальный разделитель для выделения определённых
частей формы или изображение, улучшающее внешний вид формы. Подобный HTML код
можно описать в коллекции [CForm::elements] как статический текст. Для этого
в [CForm::elements] в нужном нам месте вместо массива необходимо использовать строку.
Например:

~~~
[php]
return array(
    'elements'=>array(
		......
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),

        '<hr />',

        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),
	......
);
~~~

В приведённом коде мы вставили горизонтальный разделитель между полями `password` и `rememberMe`.

Статический текст лучше всего использовать в том случае, когда разметка и её расположение
достаточно уникальны. Если некоторую разметку должен содержать каждый элемент формы,
лучше всего переопределить непосредственно построение разметки формы, как будет описано далее.


### Описание вложенных форм

Вложенные формы используются для разделения сложных форм на несколько связанных простых. К примеру,
мы можем разделить форму регистрации пользователя на две вложенные формы: данные для входа и
данные профиля. Каждая вложенная форма может (хотя и не обязана) быть связана с моделью данных.
В примере с формой регистрации, если мы храним данные для входа и данные профиля в
двух разных таблицах (и, соответственно, в двух моделях), то каждая вложенная форма
будет сопоставлена соответствующей модели данных. Если же все данные хранятся в
одной таблице, ни одна из вложенных форм не будет привязана к модели и обе будут
использовать модель главной формы.

Вложенная форма, как и главная, описывается объектом [CForm]. Для того чтобы
описать вложенную форму, необходимо определить элемент типа `form` в
свойстве [CForm::elements]:

~~~
[php]
return array(
    'elements'=>array(
		......
        'user'=>array(
            'type'=>'form',
            'title'=>'Данные для входа',
            'elements'=>array(
            	'username'=>array(
            		'type'=>'text',
            	),
            	'password'=>array(
            		'type'=>'password',
            	),
            	'email'=>array(
            		'type'=>'text',
            	),
            ),
        ),

        'profile'=>array(
        	'type'=>'form',
        	......
        ),
        ......
    ),
	......
);
~~~

Так же как и у главной формы, у вложенной формы необходимо задать свойство [CForm::elements].
Если вложенной форме необходимо сопоставить модель данных, то это можно сделать, задав свойство [CForm::model].

В некоторых случаях бывает полезно определить форму в объекте класса, отличного от [CForm].
К примеру, как будет показано ниже, можно расширить [CForm] для реализации своего
алгоритма построения разметки. При указании типа элемента `form`, вложенная форма
будет автоматически использовать объект того же класса, что и у главной формы.
Если указать тип элемента как, например, `XyzForm` (строка, оканчивающаяся на `Form`),
то вложенная форма будет использовать объект класса `XyzForm`.


Доступ к элементам формы
------------------------

Обращаться к элементам формы так же просто, как и к элементам массива. Свойство [CForm::elements] возвращает
объект [CFormElementCollection], наследуемый от [CMap], что позволяет получить доступ к элементам формы как к
элементам массива. Таким образом, чтобы обратиться к элементу `username` формы `login` из вышеприведённого примера,
можно использовать следующий код:

~~~
[php]
$username = $form->elements['username'];
~~~

Аналогично, для доступа к элементу `email` формы регистрации, можно использовать следующий код:
~~~
[php]
$email = $form->elements['user']->elements['email'];
~~~

Так как [CForm] реализует доступ к элементам [CForm::elements] как к массиву, можно упростить
приведённый код до:

~~~
[php]
$username = $form['username'];
$email = $form['user']['email'];
~~~


Создание вложенной формы
------------------------

Ранее мы уже описывали вложенные формы. Форма, содержащая вложенные формы, называется главной.
В данном разделе мы будем использовать форму регистрации пользователя в качестве примера
создания вложенных форм, соответствующих нескольким моделям данных. Далее данные для входа
пользователя хранятся в модели `User`, а данные профиля — в модели `Profile`.

Реализуем действие `register` следующим образом:

~~~
[php]
public function actionRegister()
{
	$form = new CForm('application.views.user.registerForm');
	$form['user']->model = new User;
	$form['profile']->model = new Profile;
	if($form->submitted('register') && $form->validate())
	{
		$user = $form['user']->model;
		$profile = $form['profile']->model;
		if($user->save(false))
		{
			$profile->userID = $user->id;
			$profile->save(false);
			$this->redirect(array('site/index'));
		}
	}

	$this->render('register', array('form'=>$form));
}
~~~

Выше мы создаём форму, используя настройки из `application.views.user.registerForm`.
После отправки данных формы и успешной их валидации мы пытаемся сохранить модели
пользовательских данных и профиля. Мы получаем модели через свойство `model` соответствующего
объекта вложенной формы. Так как валидация уже пройдена, мы вызываем `$user->save(false)`
с параметром `false`, позволяющим не проводить её повторно. Точно так же поступаем с
моделью профиля.

Далее описываем настройки формы в файле `protected/views/user/registerForm.php`:

~~~
[php]
return array(
    'elements'=>array(
        'user'=>array(
            'type'=>'form',
            'title'=>'Данные для входа',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                )
            ),
        ),

        'profile'=>array(
            'type'=>'form',
            'title'=>'Профиль',
            'elements'=>array(
                'firstName'=>array(
                    'type'=>'text',
                ),
                'lastName'=>array(
                    'type'=>'text',
                ),
            ),
        ),
    ),

    'buttons'=>array(
        'register'=>array(
            'type'=>'submit',
            'label'=>'Зарегистрироваться',
        ),
    ),
);
~~~

При задании каждой вложенной формы мы указываем свойство [CForm::title].
По умолчанию при построении HTML-формы каждая вложенная форма будет выведена в
`fieldset` с заданным нами заголовком.

Описываем очень простой код шаблона представления `register`:

~~~
[php]
<h1>Регистрация</h1>

<div class="form">
<?php echo $form; ?>
</div>
~~~


Настройка отображения формы
--------------------

Главное преимущество при использовании конструктора форм — разделение логики
(конфигурация формы хранится в отдельном файле) и отображения (метод [CForm::render]).
Мы можем настроить рендеринг формы, переопределив метод [CForm::render] либо используя собственный файл представления.
Оба варианта позволяют не менять конфигурацию формы и использовать её повторно.

При переопределении [CForm::render] необходимо, главным образом, обойти коллекции [CForm::elements]
и [CForm::buttons] и вызвать метод [CFormElement::render] для каждого элемента. Например:

~~~
[php]
class MyForm extends CForm
{
	public function render()
	{
		$output = $this->renderBegin();

		foreach($this->getElements() as $element)
			$output .= $element->render();

		$output .= $this->renderEnd();

		return $output;
	}
}
~~~

Также можно использовать представление `_form`:

~~~
[php]
<?php
echo $form->renderBegin();

foreach($form->getElements() as $element)
	echo $element->render();

echo $form->renderEnd();
~~~

Для этого достаточно написать:

~~~
[php]
<div class="form">
<?php $this->renderPartial('_form', array('form'=>$form)); ?>
</div>
~~~

Если стандартный рендеринг формы не подходит (к примеру, в форме
нужны уникальные декоративные элементы для определённых полей),
в представлении можно поступить следующим образом:

~~~
[php]
какие-нибудь сложные элементы интерфейса

<?php echo $form['username']; ?>

какие-нибудь сложные элементы интерфейса

<?php echo $form['password']; ?>

какие-нибудь сложные элементы интерфейса
~~~

В этом случае конструктор форм не очень эффективен, так как нам приходится описывать
те же объёмы кода формы. Тем не менее, преимущество есть. Оно в том, что форма,
описанная в отдельном файле конфигурации, позволяет разработчику сфокусироваться на логике.