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,3k
    • 5
    Поделиться публикацией

    Похожие публикации

    Комментарии 5
      0
      Отличная работа. Но PHPшникам, похоже, интереснее читать о выходе новых версий WordPress и Joomla. :)
        0
        Сколько у вас времени ушло на написание этой статьи??
          0
          Затрудняюсь сказать, много.
          0
          Спасибо, обе статьи очень интересные, хотя, как оказалось, и описывают эзотерическую систему.

          Что касается стандартного роутинга SF2, то вы не вполне справедливы. Мы можем создать свой загрузчик (http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html), который может брать информацию для маршрутов из, например, доктрины. Только для продакшена придется постоянно чистить/инвалидировать/пересоздавать кеш маршрутизатора вручную, при изменениях в меню.
            0
            Спасибо за перевод sysmagazine.com/posts/211086

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое