Генерация HTML: удобнее чем хелперы и чистый HTML

    Писать чистый HTML часто неудобно, особенно если нужно делать динамические вставки.

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

    В некоторых фреймворках есть хелперы, в частности написать эту статью меня вынудила Aura.Html. С хелперами иная история — они изначально задуманы для реального упрощения, поскольку одной командой могут генерировать хороший кусок HTML кода, но они в большинстве заточены под определённое использование, и что-то дальше этого выглядит слишком криво.

    Как более универсальное решение было бы не плохо не изобретать причудливый синтаксис, а использовать самый обычный PHP и всем знакомые примитивные CSS-селекторы.

    Размышляя в таком духе некоторое время назад я принялся пилить свой велосипед. Велосипед получился, использовался в рамках другого велосипеда, потом отделился, много раз обновлялся, и сейчас я хотел бы поделиться им с сообществом.

    Как оно работает?


    Идея была в том, чтобы сделать как можно проще:

    h::div('Content')
    

    что на выходе даст

    <div>
        Content
    </div>
    

    Это самый простой пример. Название метода — тэг, внутри передается значение. Если нужно добавить атрибутов — не проблема:

    h::div(
        'Content',
        [
            'class' => 'some-content'
        ]
    )
    

    <div class="some-content">
        Content
    </div>
    

    И можно было бы подумать, что проще уже никак, но тут на помощь приходят CSS-селекторы, и немного уличной магии:

    h::{'div.some-content'}('Content')
    

    На выходе будет то же самое. С первого взгляда может показаться немного странным, но на практике весьма удобно.

    В сравнении с Aura.Html


    В начале я упоминал Aura.Html, стоит сравнить как генерируется HTML там, и тут.
    Aura.Html (пример из документации):

    $helper->input(array(
        'type'    => 'search',
        'name'    => 'foo',
        'value'   => 'bar',
        'attribs' => array()
    ));
    

    Наш вариант:

    h::{'input[type=search][name=foo][value=bar]'}()
    

    Любой из параметров можно было вынести в массив.
    На выходе:

    <input name="foo" type="search" value="bar"> 
    

    И ещё вариант посерьезней.

    Aura.Html (пример из документации):

    $helper->input(array(
        'type'    => 'select',
        'name'    => 'foo',
        'value'   => 'bar',
        'attribs' => array(
            'placeholder' => 'Please pick one',
        ),
        'options' => array(
            'baz' => 'Baz Label',
            'dib' => 'Dib Label',
            'bar' => 'Bar Label',
            'zim' => 'Zim Label',
        ),
    ))
    

    Наш вариант:

    h::{'select[name=foo]'}([
        'in'       => [
            'Please pick one',
            'Baz Label',
            'Dib Label',
            'Bar Label',
            'Zim Label'
        ],
        'value'    => [
            '',
            'baz',
            'dib',
            'bar',
            'zim'
        ],
        'selected' => 'bar',
        'disabled' => ''
    ])
    

    Тут in используется явно, его можно использовать для передачи внутренностей тэга, как Content в примере с div выше. Используются как общие правила, так и некоторые специальные, немного подробнее о которых дальше.
    На выходе то же самое:

    <select name="foo">	
    	<option disabled value="">Please pick one</option>
    	<option value="baz">Baz Label</option>
    	<option value="dib">Dib Label</option>
    	<option selected value="bar">Bar Label</option>
    	<option value="zim">Zim Label</option>
    </select>
    

    Специальная обработка


    Все тэги следуют общим правилам обработки, но есть некоторые тэги, которые имеют дополнительные конструкции для удобства.
    Например:

    h::{'input[name=agree][type=checkbox][value=1][checked=1]'}()
    

    <input name="agree" checked type="checkbox" value="1">
    

    Работает похоже с select, в value значение, а checked проставится когда совпадет одноименный элемент передаваемого массива.

    Ещё один пример использования in и специальной обработкой input[type=radio]:

    h::{'input[type=radio]'}([
        'checked'   => 1,
        'value'     => [0, 1],
        'in'        => ['Off', 'On']
    ])
    

    <input type="radio" value="0"> Off
    <input checked type="radio" value="1"> On
    

    Никаких оберток label не добавляется специально, чтобы сделать код максимально общим и предсказуемым.

    Если нужно обработать массив


    Это, наверное, самая часто используемая вместе с контролем вложенности возможность, так как данные и правда часто приходят откуда-то в виде массива.
    Для обработки массива его можно передать прямо вместо значения:

    h::{'tr td'}([
        'First cell',
        'Second cell',
        'Third cell'
    ])
    

    Либо даже опустить лишние скобки в самом простом случае

    h::{'tr td'}(
        'First cell',
        'Second cell',
        'Third cell'
    )
    

    На выходе:

    <tr>
        <td>
            First cell
        </td>
        <td>
            Second cell
        </td>
        <td>
            Third cell
        </td>
    </tr>
    


    Каждый элемент массива будет обработан отдельно, то есть вполне законно передавать не только строки, но и некоторые атрибуты, правда, иногда это выглядит слишком монструозно:

    h::{'tr.row td.cs-left[style=text-align:left;][colspan=2]'}(
        'First cell',
        [
            'Second cell',
            [
                'class'     => 'middle-cell',
                'style'     => 'color:red;',
                'colspan'   => 1
            ]
        ],
        [
            'Third cell',
            [
                'colspan'   => false
            ]
        ]
    )
    

    Если в вызове тоже были указаны атрибуты — class и style будут расширены, остальные перезаписаны, атрибуты с логическим значением false будут удалены.

    <tr class="row">
        <td class="cs-left" colspan="2" style="text-align:left;">
            First cell
        </td>
        <td class="cs-left middle-cell" colspan="1" style="text-align:left;color:red;">
            Second cell
        </td>
        <td class="cs-left" style="text-align:left;">
            Third cell
        </td>
    </tr>
    

    С помощью волшебной палочки, которая не является привычной частью CSS-селектора (это единственное исключение, без которого можно обойтись), можно управлять тем, как будут обрабатываться уровни вложенности:

    h::{'tr| td'}([
        [
            'First row, first column',
            'First row, second column'
        ],
        [
            'Second row, first column',
            'Second row, second column'
        ]
    ])
    

    <tr>
        <td>
            First row, first column
        </td>
        <td>
            First row, second column
        </td>
    <tr>
    <tr>
        <td>
            Second row, first column
        </td>
        <td>
            Second row, second column
        </td>
    <tr>
    

    Если массив получен из базы данных, или иного хранилища — удобно использовать такой массив напрямую, и это можно сделать передав в специальный атрибут insert:

    $array = [
        [
            'text'  => 'Text1',
            'id'    => 10
        ],
        [
            'text'  => 'Text2',
            'id'    => 20
        ]
    ];
    h::a(
        '$i[text]',
        [
            'href'      => 'Page/$i[id]',
            'insert'    => $array
        ]
    )
    

    <a href="Page/10">
        Text1
    </a>
    <a href="Page/20">
        Text2
    </a>
    

    Можно и в одну строчку все атрибуты написать:

    $array = [
        [
            'id'    => 'first_checkbox',
            'value' => 1
        ],
        [
            'id'    => 'second_checkbox',
            'value' => 0
        ],
        [
            'id'    => 'third_checkbox',
            'value' => 1
        ]
    ];
    h::{'input[id=$i[id]][type=checkbox][checked=$i[value]][value=1]'}([
        'insert'    => $array
    ])
    

    <input id="first_checkbox" checked type="checkbox" value="1"> 
    <input id="second_checkbox" type="checkbox" value="1"> 
    <input id="third_checkbox" checked type="checkbox" value="1">
    

    А ещё всё это можно расширять


    Этот класс представляет только общие, ни к чему не привязанные правила генерации HTML, которые могут быть использованы независимо от окружения.
    Но иногда хочется упростить выполнение более сложных рутинных операций.
    Например, я использую многие элементы UIkit на фронтенде, и, например, для переключателя нужна особым образом подготовленный HTML.
    Скопировав оригинальный код обработки input и слегка отредактировав можно получить такой результат:

    h::radio([
        'checked'   => 1,
        'value'     => [0, 1],
        'in'        => ['Off', 'On']
    ])
    

    <span class="uk-button-group" data-uk-button-radio="">	
    	<label class="uk-button uk-active" for="input_544f4ae475f58">	
    		<input checked="" id="input_544f4ae475f58" type="radio" value="1"> On
    	</label>
    	<label class="uk-button" for="input_544f4ae475feb">	
    		<input id="input_544f4ae475feb" type="radio" value="0"> Off
    	</label>
    </span>
    

    Так же можно переопределить метод pre_processing, и реализовать произвольную обработку атрибутов непосредственно перед рендерингом тэга, например, при наличии атрибута data-title я навешиваю класс, и таким образом получаю всплывающую подсказку над элементом при наведении.

    Преимущество использования


    Генерируется HTML без шанса оставить тэг незакрытым, или что-то в этом роде.
    Везде используются общие правила обработки, которые логичны, весьма быстро запоминаются, и являются намного чаще удобными, чем наоборот.
    Можно использовать с абсолютно любыми тэгами, даже с веб-компонентами (пример писать не буду, и так много примеров).
    Нет никаких зависимостей, есть возможность унаследовать и переопределить/расширить по желанию всё что угодно, так как это всего лишь один статический класс, и больше ничего.
    На выходе обычная строка, которую можно легко использовать вместе с абсолютно любым кодом, использовать на входе следующего вызова класса.

    Где взять и почитать


    На этом, пожалуй, хватит примеров.
    Исходный код на GitHub
    Там же есть документация с подробным объяснением всех нюансов использования и всех поддерживаемых конструкций.
    Поставить можно через composer, либо просто подключив файл с классом.
    Пример наследования с добавлением функциональности

    Планы


    Нужно всё-таки отрефакторить __callStatic(), не сломав при этом ничего)
    Было бы круто переписать на Zephir, и сделать расширение для PHP (это скорее мечта, но, возможно, когда-то возьмусь и за нее).
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 33

      +7
      По-моему, вы придумали HAML. Ну, то есть тамошняя реализация — немного на другом уровне, но решаемая задача очень похожа. Кстати, для PHP HAML тоже есть
        0
        Идея похожа, но не совсем та.
        HAML — это как HTML, только HAML. То есть просто шаблонизатор, который нужно парсить (можно кэшировать где-то, конечно, но зачем если можно не кэшировать), передавать значения, я о шаблонизаторах в целом писал в самом начале, но HAML один из самых интересных.

        Тут же вы используете PHP, и, соответственно, при необходимости можете вызывать произвольные функции, использовать тернарный оператор, сразу же передать полученную строку куда-то. Взаимодействие получается проще.
          +1
          HAML (Jade) + Twig + Symfony.
        +17
          +4
          Это значит «велосипед… зато какой!»?
            0
            при попытке открыть ваше изображение хабраприложение вылетает с ошибкой. Интрига, а не картинка :)
            0
            Это отлично, но как быть при разделении бекенд и фронтенд на разных специалистов? Фронт может не быть силён в программировании и классическая вёрстка ему будет ближе. С другой стороны есть много шаблонизаторов (haml, jade) позволяющие создавать фрагменты вёрстки.
              0
              Мое стойкое убеждение в том, что HTML это не View, это структура для него.
              Для View существуют веб-компоненты.
              К примеру:

              h::e_profile_account_details([
              	'name'                  => $profile['name'],
              	'type_of_housing' => $profile['type_of_housing'] ?: 'Unknown',
              	'area'                    => $profile['area'],
              	'persons'              => $profile['persons']
              ])
              

              Вот таким подходом в текущем проекте генерируется блок пользователя (код схематичен). e_profile_account_details превращается в веб-компонент e-profile-account-details, а как он выглядит backend-у уже не интересно совсем, всё остальное делается на фронтенде с декларативными data-binding к созданным атрибутам, работает кеширование практически всего View на уровне браузера (прокси, CDN) и прочие радости.
              В изменении внешнего вида backend не участвует, совсем.

              Или вот ещё схематический пример:

              h::{'e-profile-home-device'}([
              	'id'			=> '$i[id]',
              	'category'		=> '$i[category]',
              	'device_name'		=> '$i[name]',
              	'insert'		=> $active_devices
              ])
              

              Как элемент выглядит, что будет при клике на нём — всё это чистый frontend, а backend только генерирует структуру того, что будет передано в браузер.

              Размер страницы получается минимальный, на backend нет кучи шаблонов, которые, возможно с модификациями, нужно тащить ещё и на frontend.
              +2
              Проблема в том, что чистый HTML писать слишком сложно? Я думал все уже давно пользуются snippet'ами для этого.
                +3
                Присоединяюсь к вопросу. Тоже когда-то экспериментировала с генерацией HTML-кода, но вскоре стало понятно, что это кодинг ради кодинга и создание ненужных проблем. Сниппеты — лучшее решение.
                  0
                  Emmet он же бывший ZenCoding. Как познакомился с ним — проблем не знаю.
                  Огромное спасибо chikuyonok за этот продукт.
                    0
                    Посмотрел дему и немного ужаснулся. Очередной новый язык изобретен. Да, довольно удобно, но запомнить все эти новые теги параллельно с HTML мой мозг отказывается. У меня проще:
                    <td3TAB ->
                    td /td
                    td /td
                    td /td

                    classTAB ->
                    class=""
                      0
                      Вру. Установил плагин для Vim, понял что все довольно просто.
                      Используется стандартный биндинг клавиш режима ввода плюс какая-то непонятная запятая в конце требуется. Я привык завершать snippet через Tab, а тут мне нужно либо какую нибудь "<C-Z>," (обратите внимание на запятую в конце!), либо "," (та же запятая в конце), но при этом UltiSnips начинает подвисать (понятно почему). Перелапачивать плагин придется.
                        0
                        Увы, не знаю как обстоят дела плагина с Vim, но пользуясь им в Notepad++, а после в Sublime Text трудностей не испытывал. И да, это больше не новый язык, а набор динамических сниппетов. Огромный его плюс в написании вложенных повторяющихся элементов. Например,
                        GIF ~650Кб

                +1
                Ммм… После того как я изобретал свой шаблонизатор я очень негативно отношусь к парсингу строк для генерации кода. Много проблем может возникнуть + никаких подсказок от IDE.
                Еще заметил что полностью генерировать весь html неудобно. Визуально много бесполезного шума получется. Остановился на варианте генерации html кода где нужно совмещать пхп-код с html-тегами. Так получается наиболее оптимально.
                Мой велосипед менее функционален, но при этом более прост в использовании и не требует никакого знания спец-вставок и их парсинга.
                Примеры:
                1. HtmlTag::a('content')->href('/url')->class('abc');
                2. HtmlTag::a(array('content' => 'text', 'href' => '/url', 'class' => 'abc'))
                В чем преимущества:
                — никакого парсинга входных строк что сильно уменьшает количество возможных багов и ускоряет генерацию кода
                — В примере 1 будут подсказки аттрибутов от IDE, что значительно ускоряет написание
                — В content можно передавать другие теги
                — Можно модифицировать аттрибуты и контент когда угодно и где угодно т.к. мы работаем с объектом (также есть методы append и prepend для контента, addClass для классов и некоторые другие помогалки)
                — Можно легко делать компоненты унаследовав класс HtmlTag. Что я и сделал для form и input, textarea, select тэгов, получив весьма удобный генератор форм с минимальными запарами.
                Недостатки:
                — нет автоматизации для массивов данных (хотя, вероятно, я ее добавлю ибо очень уж полезная идея)
                  0
                  Если вызывать методы из символов что позволяет синтаксис PHP — парсинга строки можно сказать нет, и работает быстрее, спец вставки разного рода исключительно опциональны. По атрибутам подсказок не получите, но если унаследуете и допишите перед классом PhpDoc блоки с названиями методов как тегов, кторые вам нужны (то есть будут PhpDoc блоки, а реализации самих методов не будет) — получите подсказки. Просто тэгов много, атрибутов и подавно, а в последнее время я активно использую веб-компоненты, и в итоге добавить для всего подсказки для IDE просто невозможно.
                    0
                    У меня как раз на php-doc и завязаны все вызовы HtmlTag::tagName(). Тегов и атрибутов на самом деле не так уж и много.
                    А разве в h::{'input[id=$i[id]][type=checkbox][checked=$i[value]][value=1]'}() строка 'input[id=$i[id]][type=checkbox][checked=$i[value]][value=1]' не парсится?
                    0
                    И, к стати, оно у вас может и работает быстрее, но вероятнее всего памяти кушает больше.
                      0
                      Если у вас оно сходу преобразовывается в строку, то да, моё творение жрет больше. Зато позволяет себя модифицировать далее в коде (а это весьма полезно бывает).
                      К тому же если использовать echo HtmlTag::a(), то получится примерно одинаково т.к. объект сразу превратится в строку (методом __toString()) и на него не останется ссылок (т.е. сборщик мусора его грохнет).
                      Разница в расходе памяти тут будет зависеть от реализации.
                      0
                      Реализовал аналогично, но для генерации платформонезависимого SQL. Работает вполне себе шустро, плюс ко всему, умеет генерировать объекты из строки.
                        0
                        Для SQL у меня тоже есть свой велосипед больше похожий на ORM, правда. Хотя 1 из компонентов — сборщик sql запросов DbQuery::create(table, alias)->fields('*')->where(array())->run() и т.д. Ситуация была такая, что нужно было сделать максимально вредный ORM, который умел бы превращать ответы из БД в объекты и наоборот. Валидация, конвертация типов (для timestamp, например), максимально простое прикрепление файлов и получение путей к ним и т.п. Все поля, естественно, должны быть описаны в конфигах
                        Фишка в том, что ORM должен вылетать с исключениями на каждый непонравившийся ему момент. Ну а любое исключение отправляется мне на email. Идея в том, чтобы предотвратить неадекватные или ошибочные действия с данными. Например, если в объект не загружено поле, но я к нему обращаюсь — получу исключение на почту. Ибо нефиг.
                        Платформонезависимостью не похвастаюсь, но она «условно» есть =) Просто я не проверял на других БД, кроме PostgreSQL
                          0
                          Мой генератор SQL является частью ORM. Генерация SQL это все таки одна из задач любой ORM.
                            0
                            Согласен. Хотя обычно они не связаны слишком сильно. У меня же получилось так, что sql-генератор проверяет нет ли в запросе неизвестных полей (берет он их из конфигов модели) и назначены ли кастомным полям типа COUNT(*) алиасы. Также умеет делать join'ы по названию модели беря данные о связи из конфига модели. Его теперь без напильника отдельно от всего ORM не поиспользуешь. Зато удобно =)
                              0
                              У меня за генерацию join'ов отвечает DataMapper, а SQL генератор это всего лишь фабрика для кучи классов, представляющих SQL сущности, которые имеют интерпретировать себя рекурсивно в строку.
                      –1
                      Очень элегантная реализация получилась. Молодцы!
                        0
                        *Элегантный интерфейс этих хелперов (идея). Код я не смотрел.
                        +3
                        А теперь мы захотели разных скинов или сделать новое общее подменю…
                          +4
                          Код удручает.

                          Весь модуль в виде единого god-сласса с навешанными на него всеми ответственностями, методы в сотни строк, с многоуровневыми вложенными условиями и циклами, «информативные» названия некоторых переменных: "$q", "$d" и методов: «template_1», возможность работы с модулем только статическими вызовами (как его передавать как сервис через иньекцию зависимостей ?).
                          Непонимание ряда моментов работы PHP в виде попыток оптимизации передачи массивов по ссылке (PHP сам передаёт массив фактически по ссылке и копирует его только при применении изменений к нему).

                          Нет, вы меня извините конечно, но лучше бы вы потратили это время на изучение паттернов проектирования, принципов SOLID, стандартов кодирования, и того, как не следует оптимизировать код за PHP, об этом целые статьи есть.
                            +2
                            Ох, уж это компилирование php в html. Как закончите, вот вам ссылочки:

                            twig.sensiolabs.org/doc/tags/macro.html
                            docs.emmet.io/
                              0
                              в Picolisp очень удобная генерация html. Плюс возможность смешивания высокоуровневых средств с низкоуровневыми на одной и той же странице
                                0
                                Вы пишите
                                Шаблонизаторы частично решают эту проблему, но их причудливый синтаксис нужно изучать

                                И тут же этот самый причудливый синтаксис. Может вам он и кажется не таким, но поверьте, даже HAML или Jade очевиднее. А по поводу HAML это тот же HTML, только HAML, это смотря как использовать. Те же, наследования, инклуды и циклы из Jade очень помогают при сборке просто огромного количества шаблонов.
                                Вобщем или вы не донесли проблему и предложили свою гениальную идею неподготовленной публике, или велосипед.
                                  0
                                  Думаю в целом, идея сделать для подобного функционала отдельную библиотеку, не плоха. Но, не кажется ли Вам, что этот недостающий функционал, можно было бы добавить в рамках той же самой Aura.Html. Постараюсь объяснить суть. Вместо того, чтобы городить кучу разных реализаций, не лучше ли воплотить библиотеку, которую потом можно будет подключать в Yii, Aura и т.п.?

                                  Шаблонизаторы частично решают эту проблему, но их причудливый синтаксис нужно изучать

                                  Шаблонизаторы порой могут то, чего не могут хелперы…
                                    0
                                    Я бы сказал, что функциональность Aura.Html можно реализовать в рамках BananaHTML, поскольку Aura.Html более высокоуровневая.
                                    По поводу подключения — ничто не мешает прописать в composer.json зависимость от BananaHTML и использовать библиотеку с Yii, Aura и т.п.

                                  Only users with full accounts can post comments. Log in, please.