Продолжим разглядывать Symfony CMF, реализующую концепцию платформы для построения CMS из слабосвязанных компонентов. В первой части статьи мы подробно рассмотрели схему хранения и доступа к данным, во второй части нас ждет все остальное.
Продолжение статьи выходит со значительной задержкой из-за моей лени, проблем со здоровьем и интернетом. За эти пару месяцев система доросла до версии 1.0.0, и все последующие правки в master-ветке зачем-то ломают работу системы, не будучи документированными. На случай, если кто захочет ставить систему руками, помните — опирайтесь на стабильные версии, помеченные тегами.
Самые нетерпеливые могут промотать вниз, скачать виртуальную машину с установленной системой (потребуется VirtualBox) и пощупать все самому, но для полноты опыта я бы рекомендовал сначала прочитать статью.
Итак. Что у нас по плану после хранения данных?
Скриншот главной страницы демо-проекта
Здесь все знакомо для многих — используется Twig. Гибкий, мощный, очень быстрый и лаконичный. Поддерживает разделение на блоки, наследование и компиляцию шаблонов в PHP-код. Шаблон главной страницы выглядит так:
В составе CoreBundle идет пачка расширений для Twig, которые упрощают работу с CMF и обход PHPCR-дерева, например, такие функции как
Больше тут особенно смотреть не на что, Twig – он и в Африке Twig.
Главная страница админки
Знаменитый генератор админок SonataAdminBundle выполняет ровно ту же самую функцию и в Symfony CMF, но через специальную прослойку в виде SonataDoctrinePhpcrAdminBundle. Сделано это, чтобы оригинальный бандл мог абстрагироваться от хранилища данных.
Для работы с древовидными структурами предназначен TreeBrowserBundle, работающий на jsTree.
Имеющие админ-часть компоненты, описанные ниже, обязательно подключают свои панели именно сюда. Поэтому подробно останавливаться на этом не вижу смысла, детальные скриншоты будут дальше.
Статический контент в CMS — основа всего. В Symfony CMF за статический контент отвечает ContentBundle, который обеспечивает базовую реализацию классов статических документов, включая многоязычность и связь с маршрутами.
Основой бандла является класс
Для мультиязычных документов предусмотрен
Бандлу полагается контроллер.
В современных больших сайтах количество материалов может легко измеряться тысячами. Помножить на количество переводов. Добавить необходимость постоянных правок материалов и URL-ов к ним во имя поисковой оптимизации.
При этом, заметьте, такими вещами обычно занимается администратор сайта (вебмастер, контент-менеджер, сеошник), а не разработчик.
Какие требования предъявляются к роутингу в таком случае?
Если вспомнить стандартный роутер Symfony 2, становится понятно, что такой гибкости там не достичь. Роуты явно прописаны в конфиге для каждого контроллера и пользователю менять их попросту не дают. Максимум, на что можно рассчитывать — это какой-нибудь
Давайте посмотрим, как выглядела схема функционирования на голом SF2:
Если не вдаваться в детали возможностей конфигурирования параметров, все довольно примитивно. Приходит запрос, роутер решает какой вызвать контроллер, контроллер дергает нужные данные и рендерит вьюшку, затем выдает заветный Response.
Это достаточно привычная схема.
Почему такой вариант недостаточно хорош для CMS?
Представим, что у нас есть некий
В моей практике встречались случаи, когда среди статичного контента встречались разные формочки, которые гармоничней смотрелись бы в составе раздела сайта, нежели как отдельный компонент. Например, на одном сайте банка в URL текстового раздела
Допустим,
Нужно что-то другое.
Резюмируем роутинг в SF2:
От прекрасного и мощного, но неудобного в случае с CMS роутера Symfony 2 пришлось отказаться в пользу новой концепции:
Сразу на ум приходит решение в лоб: создаем маршрут по умолчанию (
Звучит по-прежнему не очень.
Решение получше предоставлял (пока его не пометили как устаревший со времен Symfony 2.1)
До кучи — переадресация маршрутов (на другие роуты или абсолютные URL).
На данный момент для решения всех проблем с роутингом в Symfony CMF используются два компонента —
Ну вот, у нас есть бесконечное количество доступных для использования роутеров.
Теперь вспоминаем про поиск роутов в базе данных и
Что делают провайдеры? Провайдеры по запросу выдают упорядоченное подмножество маршрутов-кандидатов, которые могут подойти пришедшему запросу, а
Маршрут определяет, какой контроллер будет обрабатывать определенный запрос.
Аналогично (явно или по классу) маршрут может задавать и шаблон, с которым должна рендериться страница.
По желанию при помощи вышеупомянутого
Поддерживаются и редиректы. Вообще есть интерфейс
Еще одна важная фича, о которой может быть интересно узнать тем, кто с Symfony не работал — это двунаправленность роутера. Помимо распознавания маршрутов на основе заданных параметров, эти маршруты можно и генерировать, передавая параметры как аргументы. В отличие от стандартного роутера SF2, в качестве параметра для функции
Если для одного и того же материала подходит несколько маршрутов, предпочтительным будет считаться тот, локаль которого совпадает с локалью запроса.
Напоследок вернемся к написанному чуть ранее, разделение навигационного дерева и дерева контента. Взгляните на схему, разветвленная навигация согласно определенным правилам передает управление необходимым контроллерам и только после этого запрашивает данные:
В админке для маршрутов можно задать формат (чтоб URL заканчивался на
Вдобавок ко всему пока экспериментальный RoutingAutoBundle предлагает на основе заранее заготовленных правил генерировать маршруты для контента. За счет генерации автомаршрутов достигается гибкость: для отдельных маршрутов легко переводить псевдонимы, генерировать карту сайта и менять класс документов, на которые маршрут может ссылаться. Но в большинстве случае для простых CMS этот бандл может и не понадобиться.
На этом с гибкой маршрутизацией закончим.
Ни одна CMS не обходится без системы меню. Хотя структура меню обычно повторяет структуру контента, ему может потребоваться собственная логика, не определенная контентом или существующая в нескольких контекстах с разными опциями:
В состав Symfony CMF входит MenuBundle, инструмент, позволяющий определять собственные меню. Он расширяет известный KnpMenuBundle, дополняя его иерархическими и мультиязычными элементами и инструментами для их записи в выбранное хранилище.
При выводе меню MenuBundle опирается на дефолтные для KnpMenuBundle рендереры и хелперы. Полную документацию почитать рекомендуется, но вообще в самом простейшем случае вывод выглядит так:
Переданное функции имя меню в свою очередь будет передано реализации
В основе бандла лежит
В бандле поддерживается два типа узлов:
Класс
Для интеграции с админкой предусмотрены панели и сервисы для SonataDoctrinePhpcrAdminBundle. Панели доступны сразу, но чтобы использовать их, надо явно добавить и в дашборд.
Конфигурируется бандл как обычно, но все параметры опциональны.
Связь между роутингом, меню и контентом продемонстрирована тут:
Предусмотрен и бандл для работы с блоками. Блоки могут реализовывать какую-то логику или просто возвращать статичный контент, который можно вызвать в любом месте шаблона. BlockBundle основывается на SonataBlockBundle и где нужно, заменяет компоненты родительского бандла на свои, совместимые с PHPCR.
Типичные блоки с главной страницы
Внутри бандла представлены несколько типовых блоков:
Можно создавать и свои блоки.
Механизм кэширования вывода блоков работает поверх SonataCacheBundle, правда, в BlockBundle отсутствуют адаптеры для MongoDB, Memcached и APC — придется довольствоваться Varnish или SSI.
Выводятся блоки при помощи Twig-функции
Редактирование-на-лету реализовано с помощью нескольких компонентов.
Первый — RDFa-разметка. Это способ описать метаданные в HTML в стиле микроформатов, но с помощью атрибутов.
После этого код выше перестает быть «тупым» набором DOM-элементов, потому что информацию из атрибутов можно удобно извлечь в JS-код и связать ее с моделями и коллекциями Backbone.js при помощи VIE.js — это второй компонент.
Третьим в цепочке выступает create.js, который избавляет нас от необходимости придумывать интерфейс редактирования.
Схема работы create.js
create.js работает поверх VIE.js на jQuery-виджетах. Что он может?
Весь контент редактируется на месте, при этом за счет RDFa не приходится генерировать тонны вспомогательной HTML-разметки, как это делают некоторые CMS.
ckEditor на службе добра
Ну и замыкает список CreatePHP, библиотека, связывающая вызовы create.js и непосредственно бэкенд. Она отвечает за маппинг свойств модели на PHP к HTML-атрибутам и рендеринг сущности. Самые внимательные уже видели, что для CreatePHP существует Twig-расширение и его вызов красуется в первом же листинге этой статьи: передаем модель и указываем формат вывода. Красота.
Последние два компонента объединены для удобства в CreateBundle.
Одним из бандлов самой минималистичной реализации является бандл для работы с медиа-объектами. Ими могут быть документы, двоичные файлы, MP3, видеоролики и еще чего душа пожелает. В текущей версии поддерживается загрузка картинок и скачивание файлов, все остальное писать руками. SonataMediaBundle может помочь, тем более что есть интеграция.
Бандл обеспечивает:
А так же хелперы и адаптеры для интеграции:
Есть целая россыпь интерфейсов для создания своих медиа-классов:
Интересен подход к файловым путям. В терминологии бандла под путем к медиа-объекту понимается, например,
В Twig-расширении доступны говорящие сами за себя функции:
Прикрепить картинку к документу можно через предоставленный Form Type:
Реализованы адаптеры для медиа-браузера elFinder, библиотеки Gaufrette, дающей слой абстракции над файловой системой и LiipImagine, которая упрощает манипуляции с картинками.
Как я и говорил ранее, реализация бандла минималистичная. Достаточно сказать, что картинки скопом не загружаются, а чтобы физически удалить файл, прикрепленный к документу, надо удалить и сам документ. Гм.
Планируется (а местами в какой-то степени даже готова) интеграция с модулями:
Ну и конечно, разработка новых фич и устранение текущих недоработок.
Я тут уже на много килобайт текста распинаюсь, как все в Symfony CMF замечательно, поэтому логично будет спросить, а где же критика.
Недостатков хватает.
Symfony CMF обновляется нечасто — на гитхабе указано, что процесс выпуска новых версий аналогичен релизной схеме SF2, то есть каждые полгода (четыре месяца пишем новые фичи, два месяца фиксим баги и готовим релиз). Конечно, будут мелкие исправления, направленные на устранение уязвимостей, но в целом, если хочется новенького, придется изрядно подождать. При этом сейчас такой этап разработки, когда никто не обещает сохранение обратной совместимости между релизами любой ценой. Это значит — что работало в 1.0, в 1.1 может запросто сломаться.
Страдает документация. В вики проекта бардак, многие статьи уже надо бы и удалить, где-то написан устаревший код, да и в целом Symfony CMF Book не так дружелюбна и проста, как аналогичный сборник для SF2.
У CMF весьма высокий порог вхождения. Чтобы установить тестовую систему недостаточно «распаковать все в webroot и запустить install.php» — нужно хорошо понимать связь между компонентами и уметь обращаться с каждым из них. Любая доработка или внедрение своего кода потребуют вдумчивого изучения внутренностей. Хотя, наверно, использующих SF2 разработчиков это не испугает. А для пользователей документации нет вообще...
Вывод, напрашивающийся по уже только по скриншотам — система сырая и пока далека от товарного вида. Юзабилити админки под вопросом — вроде и опрятный Bootstrap, а вроде и чувствуется, что рука дизайнера здесь ничего не касалась. Несмотря на крутой фронтенд-редактор, для
В документации часто встречаются обещания сделать X или Y потом. Если захочется кому-то порекламировать проект — убедить в целесообразности использования получится с трудом, я думаю. Не будет модных нынче eye-candy-простынок, обещающих, как легка и весела станет ваша жизнь после установки Symfony CMF. В общем, «коробки», из которой можно достать привлекательную работающую систему, нет.И наверно не будет
Отдельно отмечу, что примеров промышленного использования Symfony CMF пока нет. Неизвестно, как система ведет себя под нагрузкой и что делать, если вдруг потребуется масштабирование (в том числе бэкенда) — эти вопросы не раскрыты в документации за исключением Cache-бандлов и установки APC.
Можно сразу скачать подготовленный мной образ виртуальной машины для VirtualBox, где установлено и настроено все, включая разные бэкенды. Для удобства можно прописать к себе в hosts-файл
Зеркала (.ova-файл, ≈ 1Gb):
Если по каким-то причинам вам не хочется с этим возиться, даю ссылку на онлайн-песочницу, но туда не залезешь поковыряться внутрь, хотя посмотреть на скорую руку тоже сгодится.
Вручную устанавливать немного муторно, поэтому подробно останавливаться на каждом шаге, описанном в инструкции я не буду. Просто кратко пройдусь по основным моментам.
Требования к машине для запуска CMF немного нестандартные, хотя никакого криминала.
Во-первых, нужно удовлетворить стандартные потребности для Symfony 2 (весьма вероятно, что с этим уже все в порядке):
Все остальное (APC и так далее) — по желанию.
Далее идут требования Symfony CMF. По умолчанию для хранения данных используется SQLite, поэтому проверьте, чтобы было установлено расширение
Чтобы использовать другие бэкенды, устанавливаем:
Также в папке с сэндбоксом лежит написанный мной скрипт
Хочу предостеречь от соблазна набрать в консоли
Итак, несмотря на многочисленные «но», проект выглядит интересно. На удивление удачно решены некоторые фундаментальные проблемы контент-менеджмента (например многоязычность и маршрутизация/меню). Разработка медленно ведется крайне ограниченным кругом людей, у которых и без того есть работа, поэтому сейчас лучшая помощь — форк на гитхабе и полезный пулл-реквест, будь то правка документации или исправление ошибок. Популяризация CMF только впереди (попробуйте поискать в сети материалы, их практически нет), вся надежда только на опенсорс и коммьюнити.
Использовать сейчас CMF в продакшене может быть рисково, но тем менее, стоит быть в курсе. Кто его знает, может с тех миллионов евро что-нибудь сюда перепадет и через пару лет мы увидим замечательный продукт?
На этом все. Небольшой список полезных ссылок:
Продолжение статьи выходит со значительной задержкой из-за моей лени, проблем со здоровьем и интернетом. За эти пару месяцев система доросла до версии 1.0.0, и все последующие правки в master-ветке зачем-то ломают работу системы, не будучи документированными. На случай, если кто захочет ставить систему руками, помните — опирайтесь на стабильные версии, помеченные тегами.
Самые нетерпеливые могут промотать вниз, скачать виртуальную машину с установленной системой (потребуется VirtualBox) и пощупать все самому, но для полноты опыта я бы рекомендовал сначала прочитать статью.
Итак. Что у нас по плану после хранения данных?
Скриншот главной страницы демо-проекта
Шаблонизатор
Здесь все знакомо для многих — используется Twig. Гибкий, мощный, очень быстрый и лаконичный. Поддерживает разделение на блоки, наследование и компиляцию шаблонов в PHP-код. Шаблон главной страницы выглядит так:
{% extends "SandboxMainBundle::skeleton.html.twig" %}
{% block content %}
<p><em>We are on the homepage which uses a special template</em></p>
{% createphp cmfMainContent as="rdf" %}
{{ rdf|raw }}
{% endcreatephp %}
<hr/>
{{ sonata_block_render({ 'name': 'additionalInfoBlock' }, {
'divisible_by': 3,
'divisible_class': 'row',
'child_class': 'span3'
}) }}
<div class="row">
<div class="span3">
<h2>Some additional links:</h2>
<ul>
{% for child in cmf_children(cmf_find('/cms/simple')) %}
<li>
<a href="{{ path(child) }}">{{ child.title|striptags }}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="span3">
{{ sonata_block_render({
'name': 'rssBlock'
}) }}
</div>
</div>
{% endblock %}
В составе CoreBundle идет пачка расширений для Twig, которые упрощают работу с CMF и обход PHPCR-дерева, например, такие функции как
cmf_prev
, cmf_next
, cmf_children
и другие.Больше тут особенно смотреть не на что, Twig – он и в Африке Twig.
Пара слов об админке
Главная страница админки
Знаменитый генератор админок SonataAdminBundle выполняет ровно ту же самую функцию и в Symfony CMF, но через специальную прослойку в виде SonataDoctrinePhpcrAdminBundle. Сделано это, чтобы оригинальный бандл мог абстрагироваться от хранилища данных.
Для работы с древовидными структурами предназначен TreeBrowserBundle, работающий на jsTree.
Имеющие админ-часть компоненты, описанные ниже, обязательно подключают свои панели именно сюда. Поэтому подробно останавливаться на этом не вижу смысла, детальные скриншоты будут дальше.
Статический контент
Статический контент в CMS — основа всего. В Symfony CMF за статический контент отвечает ContentBundle, который обеспечивает базовую реализацию классов статических документов, включая многоязычность и связь с маршрутами.
Основой бандла является класс
StaticContent
, состав которого окажется знакомым многим — говорящие сами за себя поля типа title
, body
, ссылка на родительский документ и так далее. Кроме того, он реализует два интерфейса:RouteReferrersInterface
, обеспечивает связку с маршрутамиPublishWorkflowInterface
, помогает показывать или скрывать контент с помощью заданных дат публикации
Для мультиязычных документов предусмотрен
MultilangStaticContent
— все то же самое, но добавлен перевод полей и объявление локали. Как делается перевод — мы уже видели в первой части статьи.Бандлу полагается контроллер.
ContentController
состоит из единственного indexAction
, который на входе принимает желамый документ и рендерит его на нужном языке, если с параметрами публикации все в порядке. Опционально можно задать шаблон, с которым будет выводиться страница. Если не задать — будет взят тот, что указан по умолчанию.Роутинг
В современных больших сайтах количество материалов может легко измеряться тысячами. Помножить на количество переводов. Добавить необходимость постоянных правок материалов и URL-ов к ним во имя поисковой оптимизации.
При этом, заметьте, такими вещами обычно занимается администратор сайта (вебмастер, контент-менеджер, сеошник), а не разработчик.
Какие требования предъявляются к роутингу в таком случае?
- URL задается пользователем
- поддержка многосайтовости
- поддержка многоязычности
- древовидная структура
- контент, меню и маршруты должны быть разделены
Если вспомнить стандартный роутер Symfony 2, становится понятно, что такой гибкости там не достичь. Роуты явно прописаны в конфиге для каждого контроллера и пользователю менять их попросту не дают. Максимум, на что можно рассчитывать — это какой-нибудь
/page/{slug}
, который можно править из админки.Давайте посмотрим, как выглядела схема функционирования на голом SF2:
Если не вдаваться в детали возможностей конфигурирования параметров, все довольно примитивно. Приходит запрос, роутер решает какой вызвать контроллер, контроллер дергает нужные данные и рендерит вьюшку, затем выдает заветный Response.
Это достаточно привычная схема.
Почему такой вариант недостаточно хорош для CMS?
Представим, что у нас есть некий
PageController
, который принимает в качестве аргумента URL-псевдоним страницы, сравнивает его с тем, что хранится в базе данных и выдает страничку, либо 404.В моей практике встречались случаи, когда среди статичного контента встречались разные формочки, которые гармоничней смотрелись бы в составе раздела сайта, нежели как отдельный компонент. Например, на одном сайте банка в URL текстового раздела
/credits/cash
добавлялся кредитный /calculator
, чтобы люди, прочитав необходимую информацию о кредитах, на месте могли посчитать себе нужные циферки.Допустим,
PageController
обработает первую часть URL, что делать с калькулятором, который, очевидно, будет выступать отдельным контроллером? Дописать в конфиге pattern: /credits/cash/calculator
и указать отдельный контроллер/экшен? Как-то некрасиво. Даже если расставить приоритеты между остальными маршрутами, совершенно очевидно, что гибкостью тут не пахнет — если изменится псевдоним в базе, руками придется править и конфиг.Нужно что-то другое.
Резюмируем роутинг в SF2:
- определяется, какой контроллер обслуживает запрос
- парсятся параметры URL
- если ничего не получилось, поведение по умолчанию основывается на заранее конфигурированном наборе роутов
- либо из конфигурации приложения
- либо из бандлов
- пользователи редактировать роуты не могут
- не масштабируется до очень большого количества роутов
- в CMS пользователь сам хочет решать, что по какому адресу должно лежать.
Концепция маршрутизации в Symfony CMF
От прекрасного и мощного, но неудобного в случае с CMS роутера Symfony 2 пришлось отказаться в пользу новой концепции:
- нужно отделять дерево контента от навигационного дерева
- навигационное дерево состоит из ссылок на элементы дерева контента. За счет этого легко реализуются:
- многосайтовость (настольная, планшетная, мобильные версии)
- мультиязычность
- переботка навигации требует клонирования навигационного дерева
- по готовности результат вливается обратно
Сразу на ум приходит решение в лоб: создаем маршрут по умолчанию (
/{url}
с обязательным параметром url: .*
), один контроллер для всех запросов и в зависимости от содержимого перенаправляем запрос в другие контроллеры. Но при этом никто не отменяет конфликтов с другими роутами.navigation:
pattern: "/{url}"
defaults: { _controller: service.controller:indexAction }
requirements:
url: .*
Звучит по-прежнему не очень.
Решение получше предоставлял (пока его не пометили как устаревший со времен Symfony 2.1)
DoctrineRouter
. Он уже гораздо гибче, потому что искал маршруты по URL в базе данных, при этом была готова реализация для документов через PHPCR-ODM, а еще можно приделать любую свою. Маршрут по желанию явно указывал контроллер, в противном случае использовался ControllerResolver
, который пытался сам решить, какой контроллер будет обрабатывать запрос. Были и встроенные распознаватели:- привязка узлов определенного типа к контроллеру
- привязка узлов определенного типа к шаблону и использование стандартного (generic) контроллера
До кучи — переадресация маршрутов (на другие роуты или абсолютные URL).
На данный момент для решения всех проблем с роутингом в Symfony CMF используются два компонента —
ChainRouter
и DynamicRouter
. Первый заменяет стандартный SF2-роутер и, несмотря на название, работу роутера (определение контроллера для обработки запроса) на самом деле не выполняет. Вместо этого он дает возможность добавлять свои роутеры в список-цепочку. В цепочке обработать запрос попробуют все сконфигурированные роутеры по очереди, в порядке приоритета. Сервисы роутеров ищутся по тегам.cmf_routing:
chain:
routers_by_id:
# включаем DynamicRouter с низким приоритетом
# в этом случае нединамические маршруты сработают раньше
# чтобы не допускать лишнего похода в базу данных
cmf_routing.dynamic_router: 20
# подключаем свой роутер
acme_core.my_router: 50
# дефолтный роутер включаем с высоким приоритетом
router.default: 100
services:
acme_core.my_router:
class: %my_namespace.my_router_class%
tags:
- { name: cmf_routing.router, priority: 300 }
Ну вот, у нас есть бесконечное количество доступных для использования роутеров.
Теперь вспоминаем про поиск роутов в базе данных и
DynamicRouter
. Его задачей является загрузка маршрутов из провайдера, провайдером может быть (и как правило является) база данных. В стандартной поставке есть реализации провайдеров для Doctrine PHPCR-ODM, Doctrine ORM и разумеется, можно дополнить список провайдеров, реализовав RouteProviderInterface.Что делают провайдеры? Провайдеры по запросу выдают упорядоченное подмножество маршрутов-кандидатов, которые могут подойти пришедшему запросу, а
DynamicRouter
принимает окончательное решение и сопоставляет запрос с конкретным объектом типа Route
.Маршрут определяет, какой контроллер будет обрабатывать определенный запрос.
DynamicRouter
использует несколько методов в порядке убывания приоритета: - явно:
Route
-документ сам точно объявляет конечный контроллер, если таковой возвращается из вызоваgetDefault('_controller')
. - по псевдониму: маршрут возвращается значение
getDefault('type')
, которое сопоставляется с конфигурацией изconfig.yml
- по классу:
Route
-документ должен реализоватьRouteObjectInterface
и вернуть объект дляgetContent()
. Возвращаемый тип класса опять же сопоставляется с конфигом - по умолчанию: будет использоваться дефолтный контроллер, если таковой указан сконфигурирован
Аналогично (явно или по классу) маршрут может задавать и шаблон, с которым должна рендериться страница.
По желанию при помощи вышеупомянутого
RouteObjectInterface
можно научить маршрут возвращать экземпляр модели, ассоциированный с ним.Поддерживаются и редиректы. Вообще есть интерфейс
RedirectRouteInterface
, но для PHPCR-ODM готова реализация в виде документа RedirectRoute
. Он может перенаправлять на абсолютный URI и на именованный маршрут, сгенерированный любым роутером в цепочке.Еще одна важная фича, о которой может быть интересно узнать тем, кто с Symfony не работал — это двунаправленность роутера. Помимо распознавания маршрутов на основе заданных параметров, эти маршруты можно и генерировать, передавая параметры как аргументы. В отличие от стандартного роутера SF2, в качестве параметра для функции
path()
можно передавать не только заданное в конфиге имя маршрута, но и реализацию RouteObjectInterface
, RouteReferrersInterface
(то есть объект-маршрут), либо ссылку на объект в репозитории, используя его content_id:{# myRoute это объект класса Symfony\Component\Routing\Route #}
<a href="{{ path(myRoute) }}">Read on</a>
{# Создает ссылку на / для этого сервера #}
<a href="{{ path('/cms/routes') }}">Home</a>
{# myContent реализует RouteReferrersInterface #}
<a href="{{ path(myContent) }}">Read on</a>
{# передаем ссылку на объект, который реализует ContentRepositoryInterface #}
<a href="{{ path(null, {'content_id': '/cms/content/my-content'}) }}">
Read on
</a>
Если для одного и того же материала подходит несколько маршрутов, предпочтительным будет считаться тот, локаль которого совпадает с локалью запроса.
Напоследок вернемся к написанному чуть ранее, разделение навигационного дерева и дерева контента. Взгляните на схему, разветвленная навигация согласно определенным правилам передает управление необходимым контроллерам и только после этого запрашивает данные:
В админке для маршрутов можно задать формат (чтоб URL заканчивался на
.html
, например) и конечный слэш.Вдобавок ко всему пока экспериментальный RoutingAutoBundle предлагает на основе заранее заготовленных правил генерировать маршруты для контента. За счет генерации автомаршрутов достигается гибкость: для отдельных маршрутов легко переводить псевдонимы, генерировать карту сайта и менять класс документов, на которые маршрут может ссылаться. Но в большинстве случае для простых CMS этот бандл может и не понадобиться.
На этом с гибкой маршрутизацией закончим.
Меню
Ни одна CMS не обходится без системы меню. Хотя структура меню обычно повторяет структуру контента, ему может потребоваться собственная логика, не определенная контентом или существующая в нескольких контекстах с разными опциями:
В состав Symfony CMF входит MenuBundle, инструмент, позволяющий определять собственные меню. Он расширяет известный KnpMenuBundle, дополняя его иерархическими и мультиязычными элементами и инструментами для их записи в выбранное хранилище.
При выводе меню MenuBundle опирается на дефолтные для KnpMenuBundle рендереры и хелперы. Полную документацию почитать рекомендуется, но вообще в самом простейшем случае вывод выглядит так:
{{ knp_menu_render('simple') }}
Переданное функции имя меню в свою очередь будет передано реализации
MenuProviderInterface
, которая будет решать, какое меню нужно показать.В основе бандла лежит
PhpcrMenuProvider
, реализация MenuProviderInterface
, ответственная за динамическую загрузку меню из PHPCR-хранилища. По умолчанию сервис провайдера конфигурируется параметром menu_basepath
, который указывает, где искать меню в PHPCR-дереве. При рендеринге меню передается параметр name
, который должен быть прямым потомком указанного базового пути. Это позволяет PhpcrMenuProvider
работать с несколькими иерархиями меню, используя единый механизм хранения. Вспоминая указанный выше пример использования, меню simple
должно находиться по адресу /cms/menu/simple
, если в конфигурации указано следующее:cmf_menu:
menu_basepath: /cms/menu
В бандле поддерживается два типа узлов:
MenuNode
и MultilangMenuNode
. MenuNode
содержит информацию об отдельном пункте меню: label
, uri
, список дочерних пунктов children
, ссылку на маршут, связанный Content-элемент, плюс список атрибутов attributes
, благодаря которому можно настраивать вывод меню.Класс
MultilangMenuNode
расширяет MenuNode
для поддержки мультиязычности: добавлено поле locale
для определения перевода, к которому принадлежит пункт и label
с uri
, помеченные как translated=true
. Это единственные поля, которые различаются между переводами.Для интеграции с админкой предусмотрены панели и сервисы для SonataDoctrinePhpcrAdminBundle. Панели доступны сразу, но чтобы использовать их, надо явно добавить и в дашборд.
Конфигурируется бандл как обычно, но все параметры опциональны.
Связь между роутингом, меню и контентом продемонстрирована тут:
Блоки
Предусмотрен и бандл для работы с блоками. Блоки могут реализовывать какую-то логику или просто возвращать статичный контент, который можно вызвать в любом месте шаблона. BlockBundle основывается на SonataBlockBundle и где нужно, заменяет компоненты родительского бандла на свои, совместимые с PHPCR.
Типичные блоки с главной страницы
Внутри бандла представлены несколько типовых блоков:
StringBlock
— блок с единственным полемbody
, который просто рендерит строку в шаблоне, даже не окружая ее какими-либо тегамиSimpleBlock
— кbody
добавляетсяtitle
ContainerBlock
— рендерит заданный список блоков (включая другие блоки-контейнеры)ReferenceBlock
— может только ссылаться на другой блок. При вызове срабатывает так, как если бы вызывался блок, на который указывает ссылка.ActionBlock
— рендерит результат выполнения определенного экшена из контроллера, можно передать желаемые параметры запросаRssBlock
— показывает RSS-фид с указанным шаблономImagineBlock
— используется LiipImagineBundle, чтобы выводить картинки прямиком из PHPCRSlideshowBlock
— особая разновидность блока-контейнера, которая позволяет обернуть любые блоки в разметку, чтобы можно было организовать слайдшоу. Примечательно, что JS-библиотеку для этого нужно выбрать самому, в комплекте ее нет.
Можно создавать и свои блоки.
Механизм кэширования вывода блоков работает поверх SonataCacheBundle, правда, в BlockBundle отсутствуют адаптеры для MongoDB, Memcached и APC — придется довольствоваться Varnish или SSI.
Выводятся блоки при помощи Twig-функции
sonata_block_render()
, только в отличие от оригинального бандла в качестве аргументов передается имя блока в PHPCR.Frontend/Inline Editing
Редактирование-на-лету реализовано с помощью нескольких компонентов.
Первый — RDFa-разметка. Это способ описать метаданные в HTML в стиле микроформатов, но с помощью атрибутов.
<div id="myarticle" typeof="http://rdfs.org/sioc/ns#Post" about="http://example.net/blog/news_item">
<h1 property="dcterms:title">News item title</h1>
<div property="sioc:content">News item contents</div>
</div>
После этого код выше перестает быть «тупым» набором DOM-элементов, потому что информацию из атрибутов можно удобно извлечь в JS-код и связать ее с моделями и коллекциями Backbone.js при помощи VIE.js — это второй компонент.
Третьим в цепочке выступает create.js, который избавляет нас от необходимости придумывать интерфейс редактирования.
Схема работы create.js
create.js работает поверх VIE.js на jQuery-виджетах. Что он может?
- изменять содержимое RDF-размеченных элементов с помощью редакторов — Aloha, Hallo, Redactor, ckEditor
- используя localStorage, обеспечивать поддержку сохранения-восстановления правок до того, как они уйдут в CMS
- управлять уведомлениями, появляющимиcя в процессе редактирования
- организовывать свои тулбары с нужными инструментами
- вызывать пользовательские workflow-функции типа «удалить», «снять с публикации»
Весь контент редактируется на месте, при этом за счет RDFa не приходится генерировать тонны вспомогательной HTML-разметки, как это делают некоторые CMS.
ckEditor на службе добра
Ну и замыкает список CreatePHP, библиотека, связывающая вызовы create.js и непосредственно бэкенд. Она отвечает за маппинг свойств модели на PHP к HTML-атрибутам и рендеринг сущности. Самые внимательные уже видели, что для CreatePHP существует Twig-расширение и его вызов красуется в первом же листинге этой статьи: передаем модель и указываем формат вывода. Красота.
Последние два компонента объединены для удобства в CreateBundle.
MediaBundle
Одним из бандлов самой минималистичной реализации является бандл для работы с медиа-объектами. Ими могут быть документы, двоичные файлы, MP3, видеоролики и еще чего душа пожелает. В текущей версии поддерживается загрузка картинок и скачивание файлов, все остальное писать руками. SonataMediaBundle может помочь, тем более что есть интеграция.
Бандл обеспечивает:
- базовые документы для простых моделей;
- базовые
FormType
для простых моделей; - контроллер для загрузки и скачивания файлов;
- хелпер, дающий абстракцию от загрузки на сервер;
- контроллер для отображения картинки.
А так же хелперы и адаптеры для интеграции:
- медиа-браузеров (elFinder, ckFinder, MceFileManager, и т. п.);
- библиотек для манипуляций с изображениями (Imagine, LiipImagineBundle).
Есть целая россыпь интерфейсов для создания своих медиа-классов:
MediaInterface
: базовый класс;MetadataInterface
: определение метаданных;FileInterface
: определяется как файл;ImageInterface
: определяется как картинка;FileSystemInterface
: файл хранится в файловой системе, как медиа-объект сохраняется путь к нему;BinaryInterface
: в основном используется, когда файл сохранен внути медиа-объекта;DirectoryInterface
: определяется как директория;HierarchyInterface
: медиа-объекты хранят директории, путь к медиа:/path/to/file/filename.ext
.
Интересен подход к файловым путям. В терминологии бандла под путем к медиа-объекту понимается, например,
/path/to/my/media.jpg
и различия между путями в Windows и *nix-системах нивелируются. В PHPCR такой путь может использоваться как идентификатор. Доступны несколько полезных методов:getPath
получает путь к объекту, сохраненному в PHPCR, ORM или другом Doctrine-хранилище;getUrlSafePath
трансформирует путь для безопасного использования в URL;mapPathToId
трансформирует путь в идентификатор, чтобы осуществлять поиск в Doctrine-хранилище;mapUrlSafePathToId
трансформирует URL обратно в идентификатор.
В Twig-расширении доступны говорящие сами за себя функции:
<a href="{{ cmf_media_download_url(file) }}" title="Download">Download</a>
<img src="{{ cmf_media_display_url(image) }}" alt="" />
Прикрепить картинку к документу можно через предоставленный Form Type:
use Symfony\Component\Form\FormBuilderInterface;
protected function configureFormFields(FormBuilderInterface $formBuilder)
{
$formBuilder
->add('image', 'cmf_media_image', array('required' => false))
;
}
Реализованы адаптеры для медиа-браузера elFinder, библиотеки Gaufrette, дающей слой абстракции над файловой системой и LiipImagine, которая упрощает манипуляции с картинками.
Как я и говорил ранее, реализация бандла минималистичная. Достаточно сказать, что картинки скопом не загружаются, а чтобы физически удалить файл, прикрепленный к документу, надо удалить и сам документ. Гм.
Перспективы
Планируется (а местами в какой-то степени даже готова) интеграция с модулями:
- SymfonyCmfSearchBundle (полноценный поиск, расширяет LiipSearchBundle)
- SymfonyCmfSimpleCms (простейшая CMS, поставляемая вместе с CMF)
- LuneticsLocaleBundle (автоматическое определение локали)
- другие бандлы от Sonata
Ну и конечно, разработка новых фич и устранение текущих недоработок.
Все ли так хорошо?
Я тут уже на много килобайт текста распинаюсь, как все в Symfony CMF замечательно, поэтому логично будет спросить, а где же критика.
Недостатков хватает.
Symfony CMF обновляется нечасто — на гитхабе указано, что процесс выпуска новых версий аналогичен релизной схеме SF2, то есть каждые полгода (четыре месяца пишем новые фичи, два месяца фиксим баги и готовим релиз). Конечно, будут мелкие исправления, направленные на устранение уязвимостей, но в целом, если хочется новенького, придется изрядно подождать. При этом сейчас такой этап разработки, когда никто не обещает сохранение обратной совместимости между релизами любой ценой. Это значит — что работало в 1.0, в 1.1 может запросто сломаться.
Страдает документация. В вики проекта бардак, многие статьи уже надо бы и удалить, где-то написан устаревший код, да и в целом Symfony CMF Book не так дружелюбна и проста, как аналогичный сборник для SF2.
У CMF весьма высокий порог вхождения. Чтобы установить тестовую систему недостаточно «распаковать все в webroot и запустить install.php» — нужно хорошо понимать связь между компонентами и уметь обращаться с каждым из них. Любая доработка или внедрение своего кода потребуют вдумчивого изучения внутренностей. Хотя, наверно, использующих SF2 разработчиков это не испугает. А для пользователей документации нет вообще...
Вывод, напрашивающийся по уже только по скриншотам — система сырая и пока далека от товарного вида. Юзабилити админки под вопросом — вроде и опрятный Bootstrap, а вроде и чувствуется, что рука дизайнера здесь ничего не касалась. Несмотря на крутой фронтенд-редактор, для
body
-элементов в админке предусмотрен лишь жалкий textarea высотой в две строки. Для типовых операций приходится совершать слишком много телодвижений из-за непродуманной навигации.В документации часто встречаются обещания сделать X или Y потом. Если захочется кому-то порекламировать проект — убедить в целесообразности использования получится с трудом, я думаю. Не будет модных нынче eye-candy-простынок, обещающих, как легка и весела станет ваша жизнь после установки Symfony CMF. В общем, «коробки», из которой можно достать привлекательную работающую систему, нет.
Отдельно отмечу, что примеров промышленного использования Symfony CMF пока нет. Неизвестно, как система ведет себя под нагрузкой и что делать, если вдруг потребуется масштабирование (в том числе бэкенда) — эти вопросы не раскрыты в документации за исключением Cache-бандлов и установки APC.
Перейдем к делу
Можно сразу скачать подготовленный мной образ виртуальной машины для VirtualBox, где установлено и настроено все, включая разные бэкенды. Для удобства можно прописать к себе в hosts-файл
ip_виртуалки cmf-sandbox
и зайти туда через браузер, но вообще заходить можно и просто по айпишнику, который она попробует подсказать сразу после логина (дефолтные логин и пароль: symfony
).Зеркала (.ova-файл, ≈ 1Gb):
Если по каким-то причинам вам не хочется с этим возиться, даю ссылку на онлайн-песочницу, но туда не залезешь поковыряться внутрь, хотя посмотреть на скорую руку тоже сгодится.
Вручную устанавливать немного муторно, поэтому подробно останавливаться на каждом шаге, описанном в инструкции я не буду. Просто кратко пройдусь по основным моментам.
Требования к машине для запуска CMF немного нестандартные, хотя никакого криминала.
Во-первых, нужно удовлетворить стандартные потребности для Symfony 2 (весьма вероятно, что с этим уже все в порядке):
- установить PHP 5.3.3+
- включить поддержку JSON
- включить поддержку ctype
- в
php.ini
корректно установитьdate.timezone
- поставить PDO-драйвера для Doctrine
Все остальное (APC и так далее) — по желанию.
Далее идут требования Symfony CMF. По умолчанию для хранения данных используется SQLite, поэтому проверьте, чтобы было установлено расширение
pdo_sqlite
.Чтобы использовать другие бэкенды, устанавливаем:
- Apache Jackrabbit и соответственно Java (для каждого дистрибутива свой способ установки). В корне установленной CMF должен быть скрипт
jack
, который скачает и поможет запустить Jackrabbit без лишних телодвижений. Можно воспользоваться им, но Java ставить все равно отдельно. - Midgard2 PHPCR и его расширение для PHP. К сожалению, пакетов для этого пока мало: они либо помечены, как нестабильные (я брал в sid в случае с Debian), либо собраны далеко не под все платформы, либо собраны, но для устаревших версий ОС. В целом, если поискать, можно найти и RPM, и deb-пакеты. На крайний случай расширение можно собрать из исходников, но дело бесполезное — поддержка Midgard2 в CMF все еще сломана.
Также в папке с сэндбоксом лежит написанный мной скрипт
switch_backends.py
, который сам подменит конфиг на нужный (оригинальные файлы правятся в app/config/phpcr/
) и почистит production-кэш, чтоб все это взлетело. По понятным причинам я пока закомментировал midgard-варианты — все равно они не работают.Хочу предостеречь от соблазна набрать в консоли
git pull
или composer update
— как я уже говорил в начале статьи, правки в master-ветке нарушают работоспособность системы, ждите очередного стабильного релиза.Резюме
Итак, несмотря на многочисленные «но», проект выглядит интересно. На удивление удачно решены некоторые фундаментальные проблемы контент-менеджмента (например многоязычность и маршрутизация/меню). Разработка медленно ведется крайне ограниченным кругом людей, у которых и без того есть работа, поэтому сейчас лучшая помощь — форк на гитхабе и полезный пулл-реквест, будь то правка документации или исправление ошибок. Популяризация CMF только впереди (попробуйте поискать в сети материалы, их практически нет), вся надежда только на опенсорс и коммьюнити.
Использовать сейчас CMF в продакшене может быть рисково, но тем менее, стоит быть в курсе. Кто его знает, может с тех миллионов евро что-нибудь сюда перепадет и через пару лет мы увидим замечательный продукт?
На этом все. Небольшой список полезных ссылок: