Как стать автором
Обновить

Работа со сложными декораторами в Zend Framework

Время на прочтение13 мин
Количество просмотров3.8K

Введение


Zend Framework — замечательная система. Такое мнение у меня сложилось на протяжение долгого времени тесного «общения» с этой системой. И замечательная она не в силу каких-то сверхвозможностей, предоставляемых программисту, а в силу того, что система эта удивительным образом приглашает программиста к собственному усовершенствованию для его, программиста, блага, предлагая простой и в то же время мощный фундамент для собственных разработок.
Работая над проектом с использованием Zend Framework, решил попытаться по максимуму использовать его возможности и сразу же обратил внимание на компонент Zend_Form (я намеренно называю Zend_Form компонентом, а не классом, поскольку компонент Zend_Form состоит из класса Zend_Form и целого набора сопутствующих классов и интерфейсов). В документации сказано достаточно просто: «Zend_Form упрощает создание форм и управление ими в ваших веб-приложениях». В общем-то это так, но без предварительной подготовки с вас семь потов сойдёт прежде, чем вы сможете создать и отобразить одну более или менее сложную форму. Концептуально форма в Zend Framework состоит из:
  • элементов
  • декораторов
  • фильтров
  • валидаторов
Элементы — это, собственно, то, что мы понимаем под элементами формы: поля ввода, выпадающие списки и пр.
Декоратор — это вся верстка, которая логически связана с элементом формы (окружает его), но не является его частью. Проще говоря, декоратор — оформление элемента формы.

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

Так в чём проблема?


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

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

Как мы видим, в дереве элементов есть две независимые ветки: ячейка таблицы, обрамляющая непосредственно элемент формы (input), и ячейка, обрамляющая метку (label). Беда в том, что label не является элементом формы (это также декоратор). А значит, чтобы создать такую структуру нужно выкручиваться при помощи грязных хаков:
  1. $this->setElementDecorators(array(
  2.     'ViewHelper',
  3.     array('decorator' => array('br' => 'HtmlTag'), 'options' => array('tag' => 'span', 'placement' => Zend_Form_Decorator_Abstract::APPEND)),
  4.     array('decorator' => array('tdOpen' => 'HtmlTag'), 'options' => array('tag' => 'td', 'openOnly' => true, 'placement' => Zend_Form_Decorator_Abstract::PREPEND)),
  5.     array('decorator' => array('tdClose' => 'HtmlTag'), 'options' => array('tag' => 'td', 'closeOnly' => true, 'placement' => Zend_Form_Decorator_Abstract::APPEND)),
  6.     array('decorator' => array('label' => 'Label'), 'options' => array('separator' => '*')),
  7.     'Errors',
  8.     array('decorator' => array('mainCell' => 'HtmlTag'), 'options' => array('tag' => 'td', 'class' => 'tregleft')),
  9.     array('decorator' => array('mainRowClose' => 'HtmlTag'), 'options' => array('tag' => 'tr'))
  10. ));
  11.  
  12. $usernameFieldLabel = $this->user_name->getDecorator('label');
  13. $defaultSeparator = $usernameFieldLabel->getOption('separator');
  14. $usernameFieldLabel->setOption('separator', $defaultSeparator.'
    Логин может состоять только из латинских букв и знака "_".'
    );
* This source code was highlighted with Source Code Highlighter.

Из недостатков:
  • ручная подстановка закрывающихся и открывающихся тэгов td
  • использование опции separator не по назначению
Да и просто, не каждый код, даже используя хаки, удастся вот так отобразить при помощи декораторов. Все было бы гораздо проще, если бы существовал декоратор, в который бы можно было добавлять другие декораторы.

Решение проблемы


Погуглив, я ничего нормального не нашел и решил написать свой собственный групповой декоратор.
Вот, что получилось:
  1. <?php
  2. require_once 'Zend/Form/Decorator/Abstract.php';
  3.  
  4. class Zend_Form_Decorator_GroupDecorator extends Zend_Form_Decorator_Abstract {
  5.     /**
  6.      * Items (decorators) to group
  7.      * @var array
  8.      */
  9.     protected $_items = null;
  10.     
  11.     /**
  12.      * Temporary form to perform decorators operations
  13.      * @var Zend_Form_Element
  14.      */
  15.     private $_temporaryDecoratorsContainer = null;
  16.     
  17.     /**
  18.      * Constructor
  19.      *
  20.      * @param array|Zend_Config $options
  21.      * @return void
  22.      */
  23.     public function __construct($options = null) {
  24.         parent::__construct($options);
  25.         $this->_temporaryDecoratorsContainer = new Zend_Form_Element('_temporaryDecoratorsContainer', array('DisableLoadDefaultDecorators' => true));
  26.         $this->getItems();
  27.     }
  28.     
  29.     /**
  30.      * Set items to use
  31.      *
  32.      * @param array $items
  33.      * @return Zend_Form_Decorator_GroupDecorator
  34.      */
  35.     public function setItems($items) {
  36.         $this->_items = $this->_temporaryDecoratorsContainer->clearDecorators()->addDecorators($items)->getDecorators();
  37.         return $this;
  38.     }
  39.     
  40.     /**
  41.      * Get tag
  42.      *
  43.      * If no items is registered, either via setItems() or as an option, uses empty array.
  44.      *
  45.      * @return array
  46.      */
  47.     public function getItems() {
  48.         if (null === $this->_items) {
  49.             if (null === ($items = $this->getOption('items'))) {
  50.                 $this->setItems(array());
  51.             } else {
  52.                 $this->setItems($items);
  53.                 $this->removeOption('items');
  54.             }
  55.         }
  56.         return $this->_items;
  57.     }
  58.     
  59.     public function addDecorator($decorator, $options = null) {
  60.         $this->_temporaryDecoratorsContainer->addDecorator($decorator, $options);
  61.         return $this;
  62.     }
  63.     
  64.     public function clearDecorators() {
  65.         $this->_temporaryDecoratorsContainer->clearDecorators();
  66.         $this->_items = array();
  67.     }
  68.     
  69.     public function getDecorator($index = null) {
  70.         if (null === $index) {
  71.             return $this->_items;
  72.         }
  73.         if (is_numeric($index)) {
  74.             $_items = array_values($this->_items);
  75.             return ($index < count($_items))?$_items[$index]:null;
  76.         }
  77.         if (is_string($index)) {
  78.             return (array_key_exists($index, $this->_items))?$this->_items[$index]:null;
  79.         }
  80.         return null;
  81.     }
  82.     
  83.     public function insertDecoratorBefore($index, $decorator, $options = null) {
  84.         $_decoratorsToAdd = $this->_temporaryDecoratorsContainer->clearDecorators()->addDecorator($decorator, $options)->getDecorators();
  85.         if (is_string($index)) {
  86.             $index = array_search($index, array_keys($this->_items));
  87.         }
  88.         if (false !== $index) {
  89.             $first = ($index > 0)?array_slice($this->_items, 0, $index, true):array();
  90.             $last = ($index < count($this->_items))?array_slice($this->_items, $index, null, true):array();
  91.             $this->_items = array_merge($first, (array)$_decoratorsToAdd, $last);
  92.         }
  93.         return $this;
  94.     }
  95.     
  96.     /**
  97.      * Render content wrapped in a group of decorators
  98.      *
  99.      * @param string $content
  100.      * @return string
  101.      */
  102.     public function render($content) {
  103.         $placement = $this->getPlacement();
  104.         $items = $this->getItems();
  105.         $_content = '';
  106.         foreach ($items as $_decorator) {
  107.             if ($_decorator instanceOf Zend_Form_Decorator_Interface) {
  108.                 $_decorator->setElement($this->getElement());
  109.                 $_content = $_decorator->render($_content);
  110.             }
  111.             else {
  112.                 require_once 'Zend/Form/Decorator/Exception.php';
  113.                 throw new Zend_Form_Decorator_Exception('Invalid decorator '.$_decorator.' provided; must be string or Zend_Form_Decorator_Interface');
  114.             }
  115.         }
  116.         switch ($placement) {
  117.             case self::APPEND:
  118.                 return $content . $_content;
  119.                 break;
  120.             case self::PREPEND:
  121.                 return $_content . $content;
  122.                 break;
  123.             default:
  124.                 return $_content.$content.$_content;
  125.                 break;
  126.         }
  127.     }
  128. }
  129. ?>
* This source code was highlighted with Source Code Highlighter.

Данный декоратор может:
  • Создавать и отображать декораторы произвольной вложенности, а значит и какой угодно сложности
  • Производить операции с декораторами (добавление, удаление, вставка) на лету, тем самым, позволяя редактировать групповые декораторы, заданные по умолчанию
Теперь вышеприведенный пример создания элемента формы будет выглядеть так:
  1. <?php
  2. class Form_MemberRegister extends Zend_Form {
  3.     public function init() {
  4.         $this->setDisableLoadDefaultDecorators(true);
  5.         
  6.         $this->addDecorator('FormElements')
  7.             ->addDecorator(array('table' => 'HtmlTag'), array('tag' => 'table', 'class' => 'treg'))
  8.             ->addDecorator('Form');
  9.         
  10.         $this->addElement('text', 'user_name', array('label' => 'Логин:'));
  11.         $this->addElement('password', 'password', array('label' => 'Пароль:'));
  12.         $this->addElement('password', 'password2', array('label' => 'Повторите пароль:'));
  13.         $this->addElement('text', 'email', array('label' => 'E-mail:'));
  14.         
  15.         $this->setElementDecorators(array(
  16.             array('decorator' => array('labelGroup' => 'GroupDecorator'), 'options' => array('items' => array(
  17.                 array('decorator' => 'Text', 'options' => array('text' => '*')),
  18.                 array('decorator' => array('span' => 'HtmlTag'), 'options' => array('tag' => 'span', 'class' => 'red')),
  19.                 array('decorator' => 'Label','options' => array('placement' => Zend_Form_Decorator_Abstract::PREPEND)),
  20.                 array('decorator' => array('labelCell' => 'HtmlTag'), 'options' => array('tag' => 'td', 'class' => 'tregleft'))
  21.             ))),
  22.             array('decorator' => array('elementGroup' => 'GroupDecorator'), 'options' => array('items' => array(
  23.                 'ViewHelper',
  24.                 array('decorator' => array('elementCell' => 'HtmlTag'), 'options' => array('tag' => 'td'))
  25.             )), 'placement' => Zend_Form_Decorator_Abstract::APPEND),
  26.             array('decorator' => array('mainRowClose' => 'HtmlTag'), 'options' => array('tag' => 'tr'))
  27.         ));
  28.         
  29.         /**
  30.         * @var Zend_Form_Decorator_GroupDecorator
  31.         */
  32.         $usernameFieldLabel = $this->user_name->getDecorator('labelGroup');
  33.         $usernameFieldLabel->insertDecoratorBefore('labelCell', array('usernameNotes' => $this->_getNotesDecorator($this->user_name, 'Логин может состоять только из латинских букв и знака "_".')));
  34.         
  35.         /**
  36.         * @var Zend_Form_Decorator_GroupDecorator
  37.         */
  38.         $emailFieldDecorator = $this->email->getDecorator('elementGroup');
  39.         $emailFieldDecorator->insertDecoratorBefore('elementCell', array('emailNotes' => $this->_getNotesDecorator($this->email, 'Вводите только существующий и рабочий е-маил. На этот адрес Вам будет отправлена ссылка для подтверждения регистрации.')));
  40.     }
  41.     
  42.     protected function _getNotesDecorator($element, $notesText = '') {
  43.         $_d = new Zend_Form_Decorator_GroupDecorator(array('items' => array(
  44.             array('decorator' => array('notesText' => 'Text'), 'options' => array('text' => $notesText)),
  45.             array('decorator' => array('notesTag' => 'HtmlTag'), 'options' => array('tag' => 'small')),
  46.             array(
  47.                 'decorator' => array('br' => 'HtmlTag'),
  48.                 'options' => array('tag' => 'br', 'openOnly' => true, 'placement' => Zend_Form_Decorator_Abstract::PREPEND)
  49.             )
  50.         )));
  51.         return $_d->setElement($element);
  52.     }
  53. }
  54. ?>
* This source code was highlighted with Source Code Highlighter.

Заранее прошу прощения, если пост оказался слишком длинным.
Если есть другое, более изящное решение, поставленной проблемы, дайте знать — я посыплю свою голову пеплом :)
PS: Для противников табличной верстки хочу заметить, что это пост о программировании, я не верстальщик — мне всего лишь нужно было отобразить заданный шаблон.
Теги:
Хабы:
Всего голосов 10: ↑7 и ↓3+4
Комментарии19

Публикации

Истории

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань