- PVSM.RU - https://www.pvsm.ru -
Продолжим разглядывать Symfony CMF [1], реализующую концепцию платформы для построения CMS из слабосвязанных компонентов. В первой части [2] статьи мы подробно рассмотрели схему хранения и доступа к данным, во второй части нас ждет все остальное.
Продолжение статьи выходит со значительной задержкой из-за моей лени, проблем со здоровьем и интернетом. За эти пару месяцев система доросла до версии 1.0.0, и все последующие правки в master-ветке зачем-то ломают работу системы, не будучи документированными. На случай, если кто захочет ставить систему руками, помните — опирайтесь на стабильные версии, помеченные тегами.
Самые нетерпеливые могут промотать вниз, скачать виртуальную машину с установленной системой (потребуется VirtualBox) и пощупать все самому, но для полноты опыта я бы рекомендовал сначала прочитать статью.
Итак. Что у нас по плану после хранения данных?
Скриншот главной страницы демо-проекта
Здесь все знакомо для многих — используется Twig [3]. Гибкий, мощный, очень быстрый и лаконичный. Поддерживает разделение на блоки, наследование и компиляцию шаблонов в 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
и другие [4].
Больше тут особенно смотреть не на что, Twig – он и в Африке Twig.
Главная страница админки
Знаменитый генератор админок SonataAdminBundle [5] выполняет ровно ту же самую функцию и в Symfony CMF, но через специальную прослойку в виде SonataDoctrinePhpcrAdminBundle [6]. Сделано это, чтобы оригинальный бандл мог абстрагироваться от хранилища данных.
Для работы с древовидными структурами предназначен TreeBrowserBundle, работающий на jsTree [7].
Имеющие админ-часть компоненты, описанные ниже, обязательно подключают свои панели именно сюда. Поэтому подробно останавливаться на этом не вижу смысла, детальные скриншоты будут дальше.
Статический контент в CMS — основа всего. В Symfony CMF за статический контент отвечает ContentBundle, который обеспечивает базовую реализацию классов статических документов, включая многоязычность и связь с маршрутами.
Основой бандла является класс StaticContent
, состав которого окажется знакомым многим — говорящие сами за себя поля типа title
, body
, ссылка на родительский документ и так далее. Кроме того, он реализует два интерфейса:
RouteReferrersInterface
, обеспечивает связку с маршрутамиPublishWorkflowInterface
, помогает показывать или скрывать контент с помощью заданных дат публикации
Для мультиязычных документов предусмотрен MultilangStaticContent
— все то же самое, но добавлен перевод полей и объявление локали. Как делается перевод — мы уже видели в первой части статьи.
Бандлу полагается контроллер. ContentController
состоит из единственного indexAction
, который на входе принимает желамый документ и рендерит его на нужном языке, если с параметрами публикации все в порядке. Опционально можно задать шаблон, с которым будет выводиться страница. Если не задать — будет взят тот, что указан по умолчанию.
В современных больших сайтах количество материалов может легко измеряться тысячами. Помножить на количество переводов. Добавить необходимость постоянных правок материалов и URL-ов к ним во имя поисковой оптимизации.
При этом, заметьте, такими вещами обычно занимается администратор сайта (вебмастер, контент-менеджер, сеошник), а не разработчик.
Какие требования предъявляются к роутингу в таком случае?
Если вспомнить стандартный роутер Symfony 2, становится понятно, что такой гибкости там не достичь. Роуты явно прописаны в конфиге для каждого контроллера и пользователю менять их попросту не дают. Максимум, на что можно рассчитывать — это какой-нибудь /page/{slug}
, который можно править из админки.
Давайте посмотрим, как выглядела схема функционирования на голом SF2:
Если не вдаваться в детали возможностей конфигурирования параметров, все довольно примитивно. Приходит запрос, роутер решает какой вызвать контроллер, контроллер дергает нужные данные и рендерит вьюшку, затем выдает заветный Response.
Это достаточно привычная схема.
Почему такой вариант недостаточно хорош для CMS?
Представим, что у нас есть некий PageController
, который принимает в качестве аргумента URL-псевдоним страницы, сравнивает его с тем, что хранится в базе данных и выдает страничку, либо 404.
В моей практике встречались случаи, когда среди статичного контента встречались разные формочки, которые гармоничней смотрелись бы в составе раздела сайта, нежели как отдельный компонент. Например, на одном сайте банка в URL текстового раздела /credits/cash
добавлялся кредитный /calculator
, чтобы люди, прочитав необходимую информацию о кредитах, на месте могли посчитать себе нужные циферки.
Допустим, PageController
обработает первую часть URL, что делать с калькулятором, который, очевидно, будет выступать отдельным контроллером? Дописать в конфиге pattern: /credits/cash/calculator
и указать отдельный контроллер/экшен? Как-то некрасиво. Даже если расставить приоритеты между остальными маршрутами, совершенно очевидно, что гибкостью тут не пахнет — если изменится псевдоним в базе, руками придется править и конфиг.
Нужно что-то другое.
Резюмируем роутинг в SF2:
От прекрасного и мощного, но неудобного в случае с CMS роутера Symfony 2 пришлось отказаться в пользу новой концепции:
Сразу на ум приходит решение в лоб: создаем маршрут по умолчанию (/{url}
с обязательным параметром url: .*
), один контроллер для всех запросов и в зависимости от содержимого перенаправляем запрос в другие контроллеры. Но при этом никто не отменяет конфликтов с другими роутами.
navigation:
pattern: "/{url}"
defaults: { _controller: service.controller:indexAction }
requirements:
url: .*
Звучит по-прежнему не очень.
Решение получше предоставлял (пока его не пометили как устаревший со времен Symfony 2.1) DoctrineRouter
. Он уже гораздо гибче, потому что искал маршруты по URL в базе данных, при этом была готова реализация для документов через PHPCR-ODM, а еще можно приделать любую свою. Маршрут по желанию явно указывал контроллер, в противном случае использовался ControllerResolver
, который пытался сам решить, какой контроллер будет обрабатывать запрос. Были и встроенные распознаватели:
До кучи — переадресация маршрутов (на другие роуты или абсолютные URL).
На данный момент для решения всех проблем с роутингом в Symfony CMF используются два компонента — ChainRouter
и DynamicRouter
. Первый заменяет стандартный SF2-роутер и, несмотря на название, работу роутера (определение контроллера для обработки запроса) на самом деле не выполняет. Вместо этого он дает возможность добавлять свои роутеры в список-цепочку. В цепочке обработать запрос попробуют все сконфигурированные роутеры по очереди, в порядке приоритета. Сервисы роутеров ищутся по тегам.
cmf_routing:
chain:
routers_by_id:
# включаем the 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 [8].
Что делают провайдеры? Провайдеры по запросу выдают упорядоченное подмножество маршрутов-кандидатов, которые могут подойти пришедшему запросу, а 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 это объект класса SymfonyComponentRoutingRoute #}
<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 [9] предлагает на основе заранее заготовленных правил генерировать маршруты для контента. За счет генерации автомаршрутов достигается гибкость: для отдельных маршрутов легко переводить псевдонимы, генерировать карту сайта и менять класс документов, на которые маршрут может ссылаться. Но в большинстве случае для простых CMS этот бандл может и не понадобиться.
На этом с гибкой маршрутизацией закончим.
Ни одна CMS не обходится без системы меню. Хотя структура меню обычно повторяет структуру контента, ему может потребоваться собственная логика, не определенная контентом или существующая в нескольких контекстах с разными опциями:
В состав Symfony CMF входит MenuBundle, инструмент, позволяющий определять собственные меню. Он расширяет известный KnpMenuBundle [10], дополняя его иерархическими и мультиязычными элементами и инструментами для их записи в выбранное хранилище.
При выводе меню MenuBundle опирается на дефолтные для KnpMenuBundle рендереры и хелперы. Полную документацию [11] почитать рекомендуется, но вообще в самом простейшем случае вывод выглядит так:
{{ 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. Панели доступны сразу, но чтобы использовать их, надо явно добавить и в дашборд.
Конфигурируется бандл как обычно [12], но все параметры опциональны.
Связь между роутингом, меню и контентом продемонстрирована тут:
Предусмотрен и бандл для работы с блоками. Блоки могут реализовывать какую-то логику или просто возвращать статичный контент, который можно вызвать в любом месте шаблона. BlockBundle основывается на SonataBlockBundle [13] и где нужно, заменяет компоненты родительского бандла на свои, совместимые с PHPCR.
Типичные блоки с главной страницы
Внутри бандла представлены несколько типовых блоков:
StringBlock
— блок с единственным полем body
, который просто рендерит строку в шаблоне, даже не окружая ее какими-либо тегамиSimpleBlock
— к body
добавляется title
ContainerBlock
— рендерит заданный список блоков (включая другие блоки-контейнеры)ReferenceBlock
— может только ссылаться на другой блок. При вызове срабатывает так, как если бы вызывался блок, на который указывает ссылка.ActionBlock
— рендерит результат выполнения определенного экшена из контроллера, можно передать желаемые параметры запросаRssBlock
— показывает RSS-фид с указанным шаблономImagineBlock
— используется LiipImagineBundle, чтобы выводить картинки прямиком из PHPCRSlideshowBlock
— особая разновидность блока-контейнера, которая позволяет обернуть любые блоки в разметку, чтобы можно было организовать слайдшоу. Примечательно, что JS-библиотеку для этого нужно выбрать самому, в комплекте ее нет.
Можно создавать и свои блоки [14].
Механизм кэширования [15] вывода блоков работает поверх SonataCacheBundle [16], правда, в BlockBundle отсутствуют адаптеры для MongoDB, Memcached и APC — придется довольствоваться Varnish или SSI.
Выводятся блоки при помощи Twig-функции sonata_block_render()
, только в отличие от оригинального бандла в качестве аргументов передается имя блока в PHPCR.
Редактирование-на-лету реализовано с помощью нескольких компонентов.
Первый — RDFa [17]-разметка. Это способ описать метаданные в 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 [18] — это второй компонент.
Третьим в цепочке выступает create.js [19], который избавляет нас от необходимости придумывать интерфейс редактирования.
Схема работы create.js
create.js работает поверх VIE.js на jQuery-виджетах. Что он может?
Весь контент редактируется на месте, при этом за счет RDFa не приходится генерировать тонны вспомогательной HTML-разметки, как это делают некоторые CMS.
ckEditor на службе добра
Ну и замыкает список CreatePHP [20], библиотека, связывающая вызовы create.js и непосредственно бэкенд. Она отвечает за маппинг свойств модели на PHP к HTML-атрибутам и рендеринг сущности. Самые внимательные уже видели, что для CreatePHP существует Twig-расширение и его вызов красуется в первом же листинге этой статьи: передаем модель и указываем формат вывода. Красота.
Последние два компонента объединены для удобства в CreateBundle.
Одним из бандлов самой минималистичной реализации является бандл для работы с медиа-объектами. Ими могут быть документы, двоичные файлы, MP3, видеоролики и еще чего душа пожелает. В текущей версии поддерживается загрузка картинок и скачивание файлов, все остальное писать руками. SonataMediaBundle [21] может помочь, тем более что есть интеграция.
Бандл обеспечивает:
FormType
для простых моделей;А так же хелперы и адаптеры для интеграции:
Есть целая россыпь интерфейсов для создания своих медиа-классов:
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 SymfonyComponentFormFormBuilderInterface;
protected function configureFormFields(FormBuilderInterface $formBuilder)
{
$formBuilder
->add('image', 'cmf_media_image', array('required' => false))
;
}
Реализованы адаптеры для медиа-браузера elFinder [22], библиотеки Gaufrette [23], дающей слой абстракции над файловой системой и LiipImagine [24], которая упрощает манипуляции с картинками.
Как я и говорил ранее, реализация бандла минималистичная. Достаточно сказать, что картинки скопом не загружаются, а чтобы физически удалить файл, прикрепленный к документу, надо удалить и сам документ. Гм.
Планируется (а местами в какой-то степени даже готова) интеграция с модулями:
Ну и конечно, разработка новых фич и устранение текущих недоработок.
Я тут уже на много килобайт текста распинаюсь, как все в 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):
Если по каким-то причинам вам не хочется с этим возиться, даю ссылку на онлайн-песочницу [27], но туда не залезешь поковыряться внутрь, хотя посмотреть на скорую руку тоже сгодится.
Вручную устанавливать немного муторно, поэтому подробно останавливаться на каждом шаге, описанном в инструкции [28] я не буду. Просто кратко пройдусь по основным моментам.
Требования к машине для запуска CMF немного нестандартные, хотя никакого криминала.
Во-первых, нужно удовлетворить стандартные потребности для Symfony 2 (весьма вероятно, что с этим уже все в порядке):
php.ini
корректно установить date.timezone
Все остальное [29] (APC и так далее) — по желанию.
Далее идут требования Symfony CMF. По умолчанию для хранения данных используется SQLite, поэтому проверьте, чтобы было установлено расширение pdo_sqlite
.
Чтобы использовать другие бэкенды, устанавливаем:
jack
, который скачает и поможет запустить Jackrabbit без лишних телодвижений. Можно воспользоваться им, но Java ставить все равно отдельно.
Также в папке с сэндбоксом лежит написанный мной скрипт switch_backends.py
, который сам подменит конфиг на нужный (оригинальные файлы правятся в app/config/phpcr/
) и почистит production-кэш, чтоб все это взлетело. По понятным причинам я пока закомментировал midgard-варианты — все равно они не работают.
Хочу предостеречь от соблазна набрать в консоли git pull
или composer update
— как я уже говорил в начале статьи, правки в master-ветке нарушают работоспособность системы, ждите очередного стабильного релиза.
Итак, несмотря на многочисленные «но», проект выглядит интересно. На удивление удачно решены некоторые фундаментальные проблемы контент-менеджмента (например многоязычность и маршрутизация/меню). Разработка медленно ведется крайне ограниченным кругом людей, у которых и без того есть работа, поэтому сейчас лучшая помощь — форк на гитхабе и полезный пулл-реквест, будь то правка документации или исправление ошибок. Популяризация CMF только впереди (попробуйте поискать в сети материалы, их практически нет), вся надежда только на опенсорс и коммьюнити.
Использовать сейчас CMF в продакшене может быть рисково, но тем менее, стоит быть в курсе. Кто его знает, может с тех миллионов евро [33] что-нибудь сюда перепадет и через пару лет мы увидим замечательный продукт?
На этом все. Небольшой список полезных ссылок:
Автор: waitekk
Источник [38]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/56774
Ссылки в тексте:
[1] Symfony CMF: http://cmf.sandbox.com
[2] первой части: http://habrahabr.ru/post/197524/
[3] Twig: http://twig.sensiolabs.org
[4] другие: http://symfony.com/doc/current/cmf/bundles/core/templating.html#walking-the-phpcr-tree
[5] SonataAdminBundle: https://github.com/sonata-project/SonataAdminBundle
[6] SonataDoctrinePhpcrAdminBundle: https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle
[7] jsTree: http://www.jstree.com/documentation
[8] RouteProviderInterface: https://github.com/symfony-cmf/Routing/blob/master/RouteProviderInterface.php
[9] RoutingAutoBundle: http://symfony.com/doc/current/cmf/bundles/routing_auto/introduction.html
[10] KnpMenuBundle: https://github.com/KnpLabs/KnpMenuBundle
[11] Полную документацию: https://github.com/KnpLabs/KnpMenuBundle/blob/master/Resources/doc/index.md
[12] как обычно: http://symfony.com/doc/current/cmf/reference/configuration/menu.html
[13] SonataBlockBundle: https://github.com/sonata-project/SonataBlockBundle
[14] свои блоки: http://symfony.com/doc/current/cmf/bundles/block/create_your_own_blocks.html
[15] Механизм кэширования: http://symfony.com/doc/current/cmf/bundles/block/cache.html
[16] SonataCacheBundle: https://github.com/vihuvac/SonataCacheBundle
[17] RDFa: http://rdfa.info/
[18] VIE.js: https://github.com/bergie/VIE
[19] create.js: http://createjs.org/
[20] CreatePHP: https://github.com/flack/createphp
[21] SonataMediaBundle: https://github.com/sonata-project/SonataMediaBundle
[22] elFinder: http://elfinder.org/
[23] Gaufrette: https://github.com/KnpLabs/Gaufrette
[24] LiipImagine: https://github.com/liip/LiipImagineBundle
[25] Яндекс.Диск: http://yadi.sk/d/_9tlJiqDH7SAL
[26] mega.co.nz: https://mega.co.nz/#!0FNBGIyT!R18SXj98cpZxgPAe3q94LFsw0H941Fkasrt29HOtFRg
[27] онлайн-песочницу: http://cmf.liip.ch/en
[28] инструкции: http://symfony.com/doc/master/cmf/book/installation.html
[29] Все остальное: http://symfony.com/doc/current/reference/requirements.html
[30] Apache Jackrabbit: http://jackrabbit.apache.org/downloads.html
[31] Midgard2 PHPCR: http://midgard-project.org/phpcr/
[32] исходников: https://github.com/midgardproject/midgard-php5
[33] тех миллионов евро: http://fabien.potencier.org/article/71/sensiolabs-raises-5-million-euros-to-boost-the-symfony-ecosystem
[34] Документация по CMF: http://symfony.com/doc/current/cmf/index.html
[35] Сайт проекта: http://cmf.symfony.com/
[36] Roadmap: https://github.com/symfony-cmf/symfony-cmf/wiki/Roadmap
[37] Для желающих поучаствовать в разработке: http://symfony.com/doc/current/cmf/contributing/index.html
[38] Источник: http://habrahabr.ru/post/211086/
Нажмите здесь для печати.