В Symfony 2 появился совершенно новый компонент для работы с формами, который, насколько я знаю, легко заменит большинство подобных библиотек для PHP и по функционалу, и по возможности в расширении оного (конечно, если не брать в расчет небольшие недостатки при работе с JavaScript). Разработка этого компонента заняла более двух лет, хотя думать над ним я начал еще где-то в 2009-ом году или даже раньше. С каждой новой версией этот компонент становится все более и более стабильным, а полностью стабильная версия ожидается с выходом Symfony 2.2.
Данный пост приурочен к выходу Zend Framework 2 Form RFC, так как мне кажется, что его разработчики, по сути, сделали много того, что уже было сделано нами. Конечно же всем ясно, что Zend Framework 2 должен обладать прослойкой для работы с формами, который полностью учитывает особенности компонентов, поставляемых с фреймворком. Целью данного поста является попытка показать, что Symfony2 Forms прекрасно подходит под эти требования. Функционал, присущий Symfony2, может быть легко убран: код для обработки форм и все уровни абстракций полностью независимы. Привязать же поддержку особенностей компонентов Zend-а так же не составит труда.
Создание обобщенной библиотеки для работы с формами, которая покрывает все возможные сценарии использования, возникающие при разработке, было непростым испытанием, долгим и сложным делом, которое, к тому же, еще не завершено. Сотрудничество и дальнейшее совместное развитие должно помочь добиться более гибкого и простого управления формами из PHP.
Начнем же мы этот пост, пожалуй, с благодарности всем тем, кто принимал участие в разработке фреймворков и библиотек для обработки форм, которые повлияли на нашу работу. Затем, перед описанием непосредственно архитектуры, я бы хотел ознакомить вас с ключевыми аспектами проектирования удобного и гибкого компонента для работы с формами.
В этом посте вы не найдете примеров использования «киллер-фич» данного компонента, для этого есть документация. Так же вы не найдете тут и информации по использованию его, так скажем, вне контекста, независимо от фреймворка. Подобная информация должна быть описана, к примеру, в Gist.
Влияние
На компонент форм повлияло множество фреймворков, написанных на разных языках, включая symfony 1, Zend Framework 1, Django, Ruby on Rails, Struts и JSF. Кроме того, в нем можно найти сходства с Formlets, WUI и iData, библиотеками для работы с формами, написанными для функциональных языков программирования, таких как Links, Curry и Clean.
Ключевые аспекты
Ключевыми аспектами при проектировании компонента для работы с формами являются:
- Повышенный уровень абстракции
- Расширяемость
- Композиционность
- Разделение задач
- Привязка моделей
- Динамические поведения
Коротко пройдемся по каждому из этих аспектов, прежде чем обсуждать, каким образом реализованная архитектура их покрывает.
Повышенный уровень абстракции
Повышение уровня абстракции позволяет взять любую часть формы — даже форму целиком — и поместить ее в структуру данных, которую затем вы сможете использовать вновь. Рассмотрим форму для выбора даты, в которой имеются три выпадающих списка для выбора по дню, месяцу и году. Для начала вам нужен код, который сформирует HTML со всеми опциями выпадающих списков. Затем, вам понадобится код, который преобразует данные из формата вашего приложения (к примеру объект DateTime, если мы говорим о PHP) в формат, подходящий для нашего представления (для того что бы отметить выбранное значение у списков). Конечно понадобится и обратная процедура. А теперь представим ситуацию, когда вам придется добавить еще один выпадающий список для каких-то дополнительных данных. В этом случае вам придется продублировать весь код для этого списка и адаптировать его под новые требования.
Введение абстракции решает эту проблему, предоставляя подходящие структуры для описания и повторного использования вашего кода.
Расширяемость
Расширяемость основывается на двух концепциях, относящихся к абстракции:
- Специализация, это логическая последовательность абстракций. Если есть возможность абстрагировать функционал в обобщенные структуры данных, то их можно так же и расширить в более специализированные структуры. Например, мы можем расширить приведенный выше пример с выбором даты, добавив поля для выбора времени. Если бы возможности расширить функционал существующего поля для выбора даты не было, то нам пришлось бы переписывать существенную часть функционала.
- Примеси являются противоположностью специализации. К примеру вы захотели изменить все существующие поле так, что бы в их описаниях была звездочка ("*"), которая бы показывала то, что эти поля обязательны к заполнению. Использовать для этого подход со специализацией несколько непрактично, так как вам придется расширить все имеющиеся поля, реализуя тот же функционал. Примеси же позволяют подключать функционал к существующим объектам без необходимости переопределять их. К тому же, добавленный таким способом функционал будет наследоваться для всех дочерних объектов в древе наследования.
Расширяемость так же относится к более углубленному расширению функционала, при помощи событий. Это мы рассмотрим позже.
Компоновщик
Если мы проанализируем последний пример, мы можем заметить, что особой разницы между полями (составными полями, такими как в рассмотренном примере, или примитивными, такими как текстовое поле ввода) и формами нету. И поля и формы
- Принимают значения по умолчанию из моделей (из массивов, из дат, из строк...)
- Преобразуют значения, что бы его можно было использовать в представлении
- Формируют HTML
- Принимают значения, введенные пользователем
- Преобразуют значение обратно в формат модели
- Могут проводить валидацию данных
Мы можем реализовать поля и формы используя одни и те же фундаментальные структуры данных. Добавив компоновщик — инструмент, позволяющий добавлять вложенные структуры (смотри шаблон проектирования «Компоновщик») — мы сможем создать форму любой сложности. Вместо того что бы разграничивать поля и формы, поговорим о формах и их составных частях. Как только у формы появляется потомок, он так же должен
- передавать значения по умолчанию (массив или объект) его потомкам
- передавать введенные значения каждого дочернего элемента обратно в массив или объект
Разделение задач
Мы можем разделить задачи, описанные выше, на несколько определенных групп:
- Преобразование данных
- Формирование HTML (представление)
- Валидация
- Привязка данных
Каждую из этих групп задач должен реализовывать отдельный компонент с явно определенным интерфейсом. Это позволит заменить любой из этих компонентов на свой: например заменить обработчик представлений, или, к примеру, прослойку валидации.
Привязка моделей
В большинстве случаев, формы напрямую привязаны к структурам, так или иначе, описанным в модели области определения. Рассмотрим на примере отправки информации профиля пользователя. Предположим, что вся информация о пользователе хранится в отдельной таблице в вашей базе данных. Таблица хранит информацию о свойствах, которые содержит профиль, о том, какой тип данных используется для каждого из свойств, их значения по умолчанию и ограничения по вводу данных. В идеале, ваше приложение так же должно содержать класс Profile со всеми необходимыми свойствами, с привязкой к таблице в базе данных, например, посредством Doctrine2 ORM. Этот класс может содержать больше информации о профиле, нежели хранится в таблице, например если в профиле указывается какой-то набор предметов, интересующих пользователя. Список этих предметов, для простоты, будем брать из файла конфигураций.
Обычно, информация описывающая объект (назовем ее метаданными) должна быть продублирована в прослойке форм. Разработчику приходится следить за тем, не изменились ли поля, какие из них можно редактировать, следить за тем, что бы форма отображала соответствующие полям HTML виджеты, следить за тем, что бы некоторые из полей не могли оставаться пустыми ну и т.д. Именно из-за этих вещей создание форм становится довольно неблагодарным делом.
Привязка моделей призвана изменить ситуацию. Она основывается на двух идеях:
- Использование существующих метаданных во время создании формы, дабы уменьшить количество дублируемого кода и/или настроек.
Значения по умолчанию должны получаться из модели области определения (в нашем случае это будет экземпляр класса Profile) и запись введенных данных так же должна осуществляться в нашу модель.
Динамические поведения
И последнее, но не наименьшее, это динамические поведения для форм. Прошли те времена, когда код для формы полностью реализовался на сервере, где проверка на безопасность введенных пользователем данных велась под жестко заданные структуры. Сегодня модно использовать JavaScript для изменения форм на стороне клиента для повышения удобства работы с системой.
Представим себе форму в виде таблицы. Каждая колонка имеет поля одного и того же типа, каждый ряд представляет собой объект на сервере. У формы имеются кнопочки, позволяющие добавлять или удалять ряды. При всем этом, должна сохраниться возможность валидации форм, они должны успешно обрабатываться на сервере.
Для реализации такой формы и были придуманы динамические поведения. Причем их использование не ограничивается относительно простыми табличными формами. Вы можете создать форму, реагирующую на любые изменения в ее структуре произведенные на стороне клиента. К сожалению, данная проблема не решена в большинстве библиотек, несмотря на востребованность таких механизмов.
Высокоуровневая архитектура
Позвольте представить высокоуровневое описание архитектуры прослойки в Symfony2, предназначенной для работы с формами. Краеугольным камнем ее служит компонент Form. Этот компонент реализует основу для составления и обработки форм, а так же использует диспетчер событий Symfony2 для обработки внутренних событий. Поверх этого компонента лежит слой подключаемых расширений:
- Расширения уровня ядра предоставляют описания структуры полей (назовем это типами форм (FormType)), реализованных во фреймворке.
- Расширение валидации необходимо для проверки введенных пользователем данных внутри форм.
- Расширение DI позволяет использовать контейнер зависимостей.
- Расширение CSRF, как можно понять из названия, добавляет CSRF защиту для формы.
- Расширения Doctrine2 (поставляемые с Doctrine Bridge) добавляют специфичные для Doctrine выпадающие списки и компоненты, позволяющие получить метаданные объекта для использования их в формах.
Верхний уровень содержит компоненты, реализующие формирование HTML-я. По умолчанию в Symfony2 есть два таких компонента: один для рендринга форм через Twig (поставляется в Twig bridge) и другой для рендринга через старый добрый PHP (поставляется с FrameworkBundle).
Наверное самое интересной особенностью данной архитектуры является то, что любой из компонентов можно заменить. Можно написать расширение для того, что бы формы использовали компонент валидации из Zend-а. Так же вы можете написать компонент представления реализующий рендринг через Smarty или любой другой шаблонизатор. Вы даже можете удалить расширение уровня ядра и написать свои основные типы форм для простых полей. Даже лежащий в основе диспетчек событий может быть заменен на ваш компонент, реализующий интерфейс EventDispatcherInterface. Это дает неплохое преимущество в гибкости, несмотря на некоторые неудобства.
Низкоуровневая архитектура
В данном разделе речь пойдет о внутренней архитектуре компонента форм. Как уже было упомянуто выше, форма и все ее части представлены в виде одной и той же структуры данных, которая реализует шаблон проектирования «Компоновщик». В компоненте Form данная структура описана интерфейсом FormInterface. Основным классом, реализующим этот интерфейс, является класс Form, который использует три компонента, при помощи которых и работает все это дело:
- Мэппер данных распределяет данные формы по ее дочерним элементам. Кроме того, в обязанности этого компонента входит сбор данных от дочерних элементов обратно к данным формы.
- Две цепочки преобразователей данных, как понятно из названия, преобразуют значения для разных их представлений. Преобразователи данных гарантируют то, что ваше приложение получит значение в строго определенном формате, вне зависимости от формата, который используется для его представления.
- Диспетчер событий позволяет запускать специфичный код на заранее определенных этапах обработки формы. Такой подход позволяет адаптировать структуру формы под входящие данные, или же отфильтровать эти самые входящие данные, произвести валидацию или изменить их.
Все эти компоненты передаются в конструктор объекта Form и не могут быть изменены после создания формы, так как это может привести к нежелательным изменениям состояния формы. Так как интерфейс конструктора объекта Form достаточно сложный, был реализован конструктор форм (FormBuilder), который упрощает создание экземпляров этих объектов.
Представление формы это структура данных, описывающая представление. То-есть вместо того, что бы в шаблонах вызывать непосредственно класс Form, вы будете работать с экземпляром класса FormView. Этот объект хранит в себе дополнительную, специфичную только для представления информацию, такую как атрибуты name для элементов HTML форм, их ID и т.д.
Приведенная ниже UML диаграмма отображает суть архитектуры.
Как видно из этой диаграммы, жизненный цикл формы состоит из трех разных представлений:
- При создании, форма представлена в виде иерархии объектов FormBuilder
- В контроллере, форма представлена иерархией объектов Form
- На уровне представления, она представлена иерархией объектов FormView
Поскольку настройка конструкторов форм их представление дело довольно нудное, в Symfony2 реализованы основные типы форм, которые уже реализуют основные настройки. Типы форм поддерживают динамическое наследование. Это значит, что вы можете расширять различные базовые типы, основываясь на опциях, передаваемых в конструктор формы. Следующая диаграмма отображает все типы, поставляемые с Symfony2 (типы, отмеченные зеленым цветом, поставляются с ядром, когда как желтые — с дополнительными бандлами):
Примеси, о которых мы раньше упоминали, реализуются в SYmfony2 как, так называемые, расширения типов. Эти расширения могут быть подключены к существующим типам форм и добавляют к ним какое-либо поведение. В Symfony2, например, существуют расширения для добавления CSRF защиты, применительно к типу «form» (и как следствие, всем типам, которые наследуют этот тип).
Фабрика форм получает иерархию типов из загруженных расширений, и использует ее для настройки объектов FormBuilder и FormView. Важно отметить, что этот этап можно контролировать. Например, тип "choice" позволяет указывать в настройках атрибут "choices", который содержит в себе все значения для опций, которые должны быть отображены.
Есть еще одна достаточно важная концепция компонента Form — предсказатели типов. Эти «предсказатели» пытаются предугадать тип и опции поля формы, основываясь на метаданных, взятых из объекта области определения, за которой закреплена форма (если, конечно, она закреплена). К примеру, если какое-либо из свойств нашего объекта содержит связь «один ко многим» между объектом Tag, предсказатели типов автоматически настроят это поле формы для этого свойства так, что бы оно было полем выбора, с возможностью выбрать несколько значений, а так же будет использовать все экземпляры объектов Tag в качестве опций для этого поля. Похожая концепция используется в ModelForms во фреймворке Django. Правда есть одно достаточно большое отличие: предсказатели типов используют метаданные предоставляемые не только ORM-ом, но и все метаданные объекта. Symfony2 поставляется с тремя предсказателями типов: один использует метаданные, предоставляемые Doctrine2, другой — предоставляемые Propel и еще один использует правила валидации.
Описанные выше концепции можно описать следующей UML диаграммой.
Заключение
То что я хотел показать в этом посте, так это то, что компонент Symfony2 Form обладает грамотно спроектированной архитектурой, которая покрывает множество важных аспектах обработки форм.
Эта архитектура решает проблемы недостаточного уровня абстракций, специализации и примесей, предоставляя динамическое древо наследования типов форм и их расширений. Она решает проблему композиционности, распределяя задачи по обработке формы на все ее элементы. Так же она обеспечивает четкое разделение задач между компонентами, благодаря чему вы можете заменить их на свои. Используя метаданные объектов области определения при создании формы, описанная архитектура реализует привязку данных к этим моделям, что позволяет производить чтение и запись данных напрямую. Так же мы не обошли вниманием и динамические поведения, предоставляя возможность отслеживать определенные события, возникающие в процессе обработки формы, используя свои обработчики, например для валидации или фильтрации данных.
Заинтересовались? Изучайте код. Экспериментируйте с ним. И помогите нам интегрировать его в ваш любимый фреймворк.
От переводчика: переводы данной статьи существуют, но мне они показались не слишком качественными. Да и думаю не помешает. В переводе возможны неточности, посему буду благодарен, если вы укажите на них в ЛС. Спасибо за внимание.
Автор: Fesor