CMS
PHP
Symfony
11 March 2014

Symfony CMF. Часть 2 и последняя

imageПродолжим разглядывать Symfony CMF, реализующую концепцию платформы для построения CMS из слабосвязанных компонентов. В первой части статьи мы подробно рассмотрели схему хранения и доступа к данным, во второй части нас ждет все остальное.

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

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

Итак. Что у нас по плану после хранения данных?


Hello worldСкриншот главной страницы демо-проекта

Шаблонизатор


Здесь все знакомо для многих — используется 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.

Пара слов об админке


Sonata
Главная страница админки

Знаменитый генератор админок SonataAdminBundle выполняет ровно ту же самую функцию и в Symfony CMF, но через специальную прослойку в виде SonataDoctrinePhpcrAdminBundle. Сделано это, чтобы оригинальный бандл мог абстрагироваться от хранилища данных.

Для работы с древовидными структурами предназначен TreeBrowserBundle, работающий на jsTree.

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

Статический контент


Статический контент в CMS — основа всего. В Symfony CMF за статический контент отвечает ContentBundle, который обеспечивает базовую реализацию классов статических документов, включая многоязычность и связь с маршрутами.

Основой бандла является класс StaticContent, состав которого окажется знакомым многим — говорящие сами за себя поля типа title, body, ссылка на родительский документ и так далее. Кроме того, он реализует два интерфейса:

  • RouteReferrersInterface, обеспечивает связку с маршрутами
  • PublishWorkflowInterface, помогает показывать или скрывать контент с помощью заданных дат публикации

publish workflow

Для мультиязычных документов предусмотрен MultilangStaticContent — все то же самое, но добавлен перевод полей и объявление локали. Как делается перевод — мы уже видели в первой части статьи.

Бандлу полагается контроллер. ContentController состоит из единственного indexAction, который на входе принимает желамый документ и рендерит его на нужном языке, если с параметрами публикации все в порядке. Опционально можно задать шаблон, с которым будет выводиться страница. Если не задать — будет взят тот, что указан по умолчанию.

Роутинг


В современных больших сайтах количество материалов может легко измеряться тысячами. Помножить на количество переводов. Добавить необходимость постоянных правок материалов и URL-ов к ним во имя поисковой оптимизации.
При этом, заметьте, такими вещами обычно занимается администратор сайта (вебмастер, контент-менеджер, сеошник), а не разработчик.

Какие требования предъявляются к роутингу в таком случае?

  • URL задается пользователем
  • поддержка многосайтовости
  • поддержка многоязычности
  • древовидная структура
  • контент, меню и маршруты должны быть разделены

Если вспомнить стандартный роутер Symfony 2, становится понятно, что такой гибкости там не достичь. Роуты явно прописаны в конфиге для каждого контроллера и пользователю менять их попросту не дают. Максимум, на что можно рассчитывать — это какой-нибудь /page/{slug}, который можно править из админки.

Давайте посмотрим, как выглядела схема функционирования на голом SF2:

Роутинг в 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.

Роутинг 2.0

Маршрут определяет, какой контроллер будет обрабатывать определенный запрос. 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. Это единственные поля, которые различаются между переводами.

Админка меню 1

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

Админка меню2

Конфигурируется бандл как обычно, но все параметры опциональны.

Связь между роутингом, меню и контентом продемонстрирована тут:

Роутинг, контент, меню

Блоки


Предусмотрен и бандл для работы с блоками. Блоки могут реализовывать какую-то логику или просто возвращать статичный контент, который можно вызвать в любом месте шаблона. BlockBundle основывается на SonataBlockBundle и где нужно, заменяет компоненты родительского бандла на свои, совместимые с PHPCR.

Блоки
Типичные блоки с главной страницы

Внутри бандла представлены несколько типовых блоков:

  • StringBlock — блок с единственным полем body, который просто рендерит строку в шаблоне, даже не окружая ее какими-либо тегами
  • SimpleBlock — к body добавляется title
  • ContainerBlock — рендерит заданный список блоков (включая другие блоки-контейнеры)
  • ReferenceBlock — может только ссылаться на другой блок. При вызове срабатывает так, как если бы вызывался блок, на который указывает ссылка.
  • ActionBlock — рендерит результат выполнения определенного экшена из контроллера, можно передать желаемые параметры запроса
  • RssBlock — показывает RSS-фид с указанным шаблоном
  • ImagineBlock — используется LiipImagineBundle, чтобы выводить картинки прямиком из PHPCR
  • SlideshowBlock — особая разновидность блока-контейнера, которая позволяет обернуть любые блоки в разметку, чтобы можно было организовать слайдшоу. Примечательно, что 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

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 в продакшене может быть рисково, но тем менее, стоит быть в курсе. Кто его знает, может с тех миллионов евро что-нибудь сюда перепадет и через пару лет мы увидим замечательный продукт?

На этом все. Небольшой список полезных ссылок:


+24
17.6k 153
Comments 5
Top of the day