 Продолжим разглядывать Symfony CMF, реализующую концепцию платформы для построения CMS из слабосвязанных компонентов. В первой части статьи мы подробно рассмотрели схему хранения и доступа к данным, во второй части нас ждет все остальное.
Продолжим разглядывать Symfony CMF, реализующую концепцию платформы для построения CMS из слабосвязанных компонентов. В первой части статьи мы подробно рассмотрели схему хранения и доступа к данным, во второй части нас ждет все остальное. Продолжение статьи выходит со значительной задержкой из-за моей лени, проблем со здоровьем и интернетом. За эти пару месяцев система доросла до версии 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, чтобы выводить картинки прямиком из 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 работает поверх 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 в продакшене может быть рисково, но тем менее, стоит быть в курсе. Кто его знает, может с тех миллионов евро что-нибудь сюда перепадет и через пару лет мы увидим замечательный продукт?
На этом все. Небольшой список полезных ссылок:

