Symfony CMF. Часть 1, хранение данных

в 6:57, , рубрики: cms, php, symfony, symfony 2, symfony cmf, метки: ,

image

Вместо предисловия

Я программирую на Yii уже два года и в последнее время начал засматриваться на Symfony Framework 2. Отчасти меня привлекает продуманная архитектура, отчасти слабая связность компонентов, отчасти гибкость построенных приложений. Сразу после того, как я разобрался с основным устройством нового фреймворка, мне стало интересно, возможно ли на нем построить CMS, а может быть, даже воспользоваться готовой.

Коробочного решения пока не придумали, однако, каким-то образом я забрел на сайт проекта Symfony CMF и оказался сражен наповал методичным подходом к решению тех проблем, с которыми я сталкивался в бытность работы на конвеере по натягиванию дизайна на какой-нибудь Друпал. На Хабре публикаций про именно CMF нет, да и сам проект еще очень сырой, однако в перспективе выглядит все интересно, хоть местами и есть к чему придраться.

Symfony CMF

Проект Symfony CMF призван упростить разработку функционала, присущего CMS, для всех, кто использует в работе Symfony Framework 2.
Основные особенности проекта:

  • слабая связность компонентов
  • масштабируемость
  • удобство
  • тестируемость

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

  • используете все, что хочется
  • заменяете то, что не нравится
  • игнорируете то, что не требуется

То есть, вам дан набор модульных инструментов для разработки, а не готовое приложение «под ключ», хотя уже разработаны базовые бандлы, обеспечивающие CMS-функционал.

Зачем еще один CMF?

Не секрет, что на рынке существует достаточно много готовых продуктов, как платных (1С-Битрикс, UMI), так и бесплатных (Drupal, MODx, WordPress, Joomla). Поэтому вполне логично, что при виде надписи Whatever CMS/CMF может возникнуть вопрос Зачем вообще делать еще одну CMS? Их же и так полно.
И я абсолютно согласен. Как пользователь.

CMS действительно пруд-пруди. Но как разработчик, я часто проливал пот, кровь и слезы, пытаясь добиться от них чего-то большего, чего-то не заложенного авторами базовой системы и сторонних расширений.

Из-за недостаточно продуманной архитектуры при работе с готовыми решениями приходится сталкиваться с:

  • отсутствием внятного разделения логики, конфигурации, содержимого и представления. Достаточно вспомнить модули Drupal — куча файлов, мешанина из непонятно как названных глобальных функций, хуков и прочего. Вот кстати неплохая статья, в которой этот вопрос обсуждается
  • много легаси-кода, остающегося со старых версий. Периодически разработчиками предпринимаются попытки исправить это, обещаются переписывания ядра и прочие радости, но пока новая (переписанная) версия дойдет до стадии «можно пользоваться», времени может пройти очень много
  • часто в системе отсуствуют такие понятия как development, testing, и отсутствуют инструменты для деплоя
  • проблемы с кэшированием. Где-то его нет, где-то оно есть, но не дает достаточную степень гибкости, или есть проблемы с инвалидацией, или просто он не спасает и т. д.
  • низкая производительность на больших (в данном случае это понятие относительное) объемах данных
  • сложность в создании своих компонентов или переопределении существующих
  • приходится выбирать между заранее определенными типами данных, либо довольствоваться EAV-хранилищами поверх реляционных СУБД, либо еще что похуже
  • неудобные шаблонизаторы-велосипеды, придуманные авторами CMS ...
  • … и все это как следствие синдрома NIH.

Разработчики этих систем в курсе недостатков и не отвергают предъявленных обвинений, однако в момент решить все эти проблемы, само собой, невозможно. Впрочем, не будем ругаться на всех подряд, лучше сформулируем ряд проблем, которые CMS должны решать в угоду удобства пользователя, а затем посмотрим, как эти проблемы решены в Symfony CMF. Итак, проблемы:

  • хранение данных
  • система шаблонов
  • маршрутизация, ЧПУ, и то, как пользователь может все это контролировать
  • настройка меню
  • контент-менеджмент (редактируемые блоки на странице, фронтенд-правки на живом сайте, заливка файлов)
  • i18n
  • хорошая админка

Начнем по порядку с проблем.

Проблема хранения данных

Исходя из собственно расшифровки понятия CMS, становится понятно, что самая важная составляющая CMS — это хранение данных. Даже больше — CMS должна обеспечивать хранение данных с разными свойствами. Например, для материалов типа BlogPost или NewsItem можно создать общие поля title и body, а дальше пойдут различия — к новостям может понадобится прикреплять картинки.

Представим себе интернет-магазин. Что хранится в базе данных? Как минимум — описания товаров и история заказов. В отличие от первого, для второго спроектировать схему хранения гораздо проще, хотя совершенно очевидно, что оба друг без дружки существовать не могут. Отсюда следующее требование: CMS должна иметь возможность ссылаться на контент как внутри CMS, так и в других частях системы.

Сам контент на сайте чаще всего организован в виде древовидной структуры, в чем-то повторяющей файловую систему. Авторы сайта при этом хотят по-разному организовывать контент в зависимости от своих нужд, а также гибко настраивать меню и адреса материалов. Таким образом, CMS должна представлять данные в виде древовидной структуры и уметь обслуживать несколько независимых деревьев одновременно.

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

В больших организациях нередки ситуации, когда материал перед появлением на сайте проходит несколько этапов проверки, вместо публикации одним кликом — CMS должна поддерживать перемещение и экспорт контента между деревьями. А для истории было бы неплохо хранить версии контента, которые можно восстановить в любой момент времени.

При этом стоит помнить про пользователей из других стран и регионов. Хоть весь сайт переводить на другой язык никто обычно не требует, CMS должна давать возможность представлять контент на разных языках, с опциональным фоллбеком по заданным правилам.

Когда контента станет слишком много, обязательно потребуется полнотекстовый поиск, возможность определять правила контроля доступа к поддеревьям, и помощь в процессе публикации документа несколькими авторами (воркфлоу у всех разный).

Content Repository

Становится ясно, что одним «мускулом» не отделаешься. Реляционные базы данных с такими задачами просто не справятся, хотя существуют алгоритмы типа Materialized path или Nested set, которые позволяют хранить в плоских базах данных структуру графа. Но даже если отдельно взятая реализация будет работать, она скорее всего будет жестко привязана к конкретному движку, а это уже плохо, потому что лишает нас свободы и гибкости. РСУБД винить не надо — они задуманы для совсем иных задач, им нужны четко описанные данные, а не деревья, состоящие из слабо структурированных элементов.

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

JCR-170

Проблема хранения данных для документо-ориентированных систем возникла много лет назад, поэтому еще в первой половине двухтысячных люди из компании Day Software (а именно — David Nüscheler) подали через Java Community Process запрос на принятие спецификации Content Repository API for Java (JCR), которой был присвоен порядковый номер 170. Позднее спецификация проходила под номером JSR-283 (2.0), JSR-333 (2.1, финальный черновик закончен 31 августа), но до сих пор чаще встречается ссылка именно на первый вариант.

Согласно спецификации, репозиторий является объектной базой данных, которая обеспечивает хранение, поиск и получение иерархических данных. Кроме того, предоставляемый API позволяет использовать версионность данных, транзакции, отслеживание изменений, импорт/экспорт в XML, а еще хранить двоичные и метаданные.

Такой репозиторий организован в виде дерева узлов, которые имеют свойства. Непосредственно данные хранятся именно в них, причем это могут быть и числа, и строки, и двоичные данные произвольной длины. Узлы могут подразделяться на типы, иметь дочерние узлы, определенные поведенческие характеристики или просто ссылаться на соседей (при помощи специального свойства и уникального идентификатора, который имеет каждый узел).

Начиная со второй версии спецификации, хранилище должно уметь отзываться на SQL-запросы, что удобней, чем их XPath-собратья из первой редакции.

Как яркий пример реализации подобного счастья можно выделить проект Apache Jackrabbit, опенсорс-репозиторий, написанный на Java. Помимо всех вышеописанных вкусностей, этот проект (начатый еще в 2004 году как начальная реализация JCR API) умеет гибко контролировать доступ к контенту. Еще там есть кластеринг, механизмы блокировок и прочее, но нам сейчас это не очень интересно, поэтому пропустим.

PHPCR

Но ведь не все пишут на Java! (Опустим шуточки на эту тему)
Для таких, как мы, был создан Content Repository for PHP — описанный выше JCR API, адаптированный под стиль PHP. Исходя из того, что API тот же самый и хорошо специфицирован, следует: можно написать приложение один раз, а потом просто менять бэкенды (теоретически, конечно).
Важный плюс — мы не изобретаем велосипед (ведь как мы помним, проблема хранения данных в CMS уже решена).
Конечно, такая инициатива не могла остаться без внимания — Дэвид отправил запрос на принятие PHPCR в JCR 2.1. Очень мило.

Поскольку нельзя просто так взять и портировать API из Java в PHP, различия между реализациями все же есть. Если коротко, связано это с тем, что PHP слабо типизирован и не поддерживает перегрузку методов. Поэтому часть интерфейсов и функций попросту выбросили за ненадобностью, а там, где была перегрузка, методам просто добавили опциональные аргументы. Подробно различия описаны здесь, но ничего страшного там нет.

На текущий момент PHPCR поддерживает следующие функции:

  • доступ к дереву
  • доступ к узлам по UUID
  • поиск по узлам
  • версионность
  • определение возможностей
  • Импорт и экспорт в XML
  • Блокировки
  • Транзакции*
  • Разрешения
  • Контроль доступа*
  • Отслеживание изменений

(*) — Еще не реализовано в Jackalope-Jackrabbit (об этом ниже), хотя информация могла немного устареть.

Ключевые концепции PHPCR:

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

Где-то мы уже это слышали, не правда ли?

Посмотрим, как может выглядеть такой репозиторий (схематично, конечно):

<root><cms><pages><home title="Hello"><block title="News" content="Today: PHPCR presentation"></block></home><contact title="Contact" content="phpcr-users@groups.google.com"></contact></pages></cms></root>

Пока ничего сверхъестественного.
Рассмотрим чуть подробнее, с чем придется работать.

Узлы

  • узел — именованный контейнер, у которого всегда есть родитель
  • напоминает XML-элементы
  • узлы можно создавать, удалять, модифицировать, копировать
  • путь к узлу состоит из пути родительского узла и имени текущего узла:
  • Путь: /cms/pages/home
  • Родительский путь: /cms/pages
  • Имя узла: home

Свойства узлов

  • у узлов есть именованные свойства, хранящие значения
  • напоминают XML-атрибуты
  • типы данных: STRING, URI, BOOLEAN, LONG, DOUBLE, DECIMAL, BINARY, DATE, NAME, PATH, WEAKREFERENCE, REFERENCE
  • типы (WEAK)REFERENCE создают ссылки на другие узлы
  • узлы и свойства могут иметь пространства имен: jcr:created, jcr:mimeType, phpcr:class

Основные типы узлов

  • определяют разрешенные для использования имена, а также типы свойств и дочерних узлов
  • у каждого узла должен быть установлен основной тип
  • для хранения-чего-угодно используется nt:unstructured
  • среди прочих встроенных типов есть nt:address, nt:folder, nt:file и другие
  • для создания своей схемы можно определять новые типы узлов

Mixin-типы узлов

  • у основных типов нет множественного наследования
  • но есть миксин-типы, которые добавляют узлам [trait](https://en.wikipedia.org/wiki/Trait_(computer_programming))-подобную функциональность
  • миксин-типы могут быть назначены узлу во время его жизни

Пример: допустим, у нас есть свойство jcr:uuid, в котором хранится уникальный идентификатор. Зная uuid, мы можем создать миксин mix:referenceable, а на его основе mix:versionable (но тогда нам еще потребуется иметь свойства jcr:versionHistory, jcr:predecessors, jcr:baseVersion, jcr:isCheckedOut, jcr:mergeFailed и т. д.)

Рабочие пространства

  • рабочих пространств может быть несколько, каждое хранит в себе свое дерево узлов
  • напоминает файловую систему Unix и ветки в Git/SVN, каждую можно клонировать и выполнять слияния
  • могут использоваться независимо

Процесс получения данных в PHPCR

А теперь немного примеров того, как со всеми этим работать:

Создание сессии

use PHPCRSimpleCredentials;

// конфигурация, зависимая от конкретной реализации бэкенда
use JackalopeRepositoryFactoryJackrabbit as Factory;
$parameters = array(
    'jackalope.jackrabbit_uri'
        => 'http://localhost:8080/server',
);
$repository = Factory::getRepository($parameters);

// а вот дальше все стандартно для любых реализаций
$creds = new SimpleCredentials('admin','admin');
$session = $repository->login($creds, 'default');

CRUD-операции

$root = $session->getRootNode();

// узлы всегда добавляются как дочерние для существующих
$node = $root->addNode('test', 'nt:unstructured');

// новый узел сразу доступен в текущей сессии
$node = $session->getNode('/test');

// создать/обновить свойство
$node->setProperty('prop', 'value');

// теперь узел доступен для всех сессий
$session->save();

// удалить узел и все дочерние узлы
$node->remove();

// выдаст ошибку, если узел кто-то редактировал в другой сессии
$session->save();

Обход дерева

$node = $session->getNode('/site/content');

foreach ($node->getNodes() as $child) {
    var_dump($child->getName());
}

// или короче
foreach ($node as $child) {
    var_dump($child->getName());
}

// фильтр по имени
foreach ($node->getNodes('di*') as $child) {
    var_dump($child->getName());
}

Версионность

// включаем версионность
$node = $session->getNode('/site/content/about');
$node->addMixin('mix:versionable');
$session->save();
// создаем начальную версию
$node->setProperty('title', 'About');
$session->save();

// чек-ин (создаем версию)
// и чек-аут (подготовка к дальнейшим обновлениям)
// результат этих операций доступен сразу же без вызова $session->save()
$vm = $session->getWorkspace()->getVersionManager();
$vm->checkpoint($node->getPath());

// обновляем узел
$node->setProperty('title', 'Ups');
$session->save();

// создаем еще одну версию, оставляем в состоянии «только для чтения»
$vm->checkin($node->getPath());

$base = $vm->getBaseVersion($node->getPath());
$current = $base->getLinearPredecessor();
$previous = $current->getLinearPredecessor();

// берем слепок старой версии
$frozenNode = $previous->getFrozenNode();
echo $frozenNode->getProperty('title'); // About

// восстанавливаем живые данные из текущей версии
$vm->restore(true, $previous);

$node = $session->getNode('/site/content/about');
echo $node->getProperty('title'); // About

Поиск

$qm = $workspace->getQueryManager();

// в SQL2 оператор звездочки "*" не возвращает все столбцы
// а по крайней мере путь и степень соответствия
// (см. http://docs.jboss.org/exojcr/1.12.13-GA/developer/en-US/html/ch-jcr-query-usecases.html#d0e3332)
$sql = "SELECT * FROM [nt:unstructured]
    WHERE [nt:unstructured].type = 'nav'
    AND ISDESCENDANTNODE('/some/path')
    ORDER BY score, [nt:unstructured].title";
$query = $qm->createQuery($sql, 'JCR-SQL2');
$query->setLimit($limit);
$query->setOffset($offset);
$queryResult = $query->execute();

foreach ($queryResult->getNodes() as $node) {
    var_dump($node->getPath());
}

Другие примеры кода можно посмотреть в этой презентации.

Однако, вернемся к манящей мысли о разных бэкендах.

Имеем мы на текущий момент не так много реализаций, но и те уже интересные:

  • Midgard2 PHPCR
  • Jackalope
  • поддерживает Jackrabbit
  • поддерживает Doctrine DBAL (хранение данных поверх реляционных БД)
  • поддерживает MongoDB (на самом деле нет)

Midgard2 PHPCR

Midgard2 — репозиторий контента с открытым исходным кодом с биндингами для C, Python и PHP.

Немного отличаясь терминологией от JCR, Midgard2 предоставляет те же самые функции для доступа к контенту через Midgard2 PHPCR с помощью расширения php5-midgard2. Будучи построенным поверх GNOME-библиотеки libgda, Midgard2 поддерживает внушительный список реляционных баз данных, в которых можно разместить свой репозиторий.

Сразу скажу про ложку дегтя — PHP-расширение собрано для достаточно малого числа ОС:

  • под Debian 7 Wheezy пакет все еще находится в нестабильных ветках (и заслуженно — молча роняет PHP-FPM в сегфолт).
  • для CentOS пакеты есть либо устаревшие, либо не для всех архитектур (но там, где есть, весьма вероятно, что работает, руки не дошли)
  • на Windows билдов не существует в природе (возможно, влияют «гномьи» корни самого Midgard2, хотя в репозитории видны четырехлетней давности файлы еще для PHP4)
  • под Mac OS потестировать не удалось ввиду отсутствия у меня оной (но судя по сайту, все ставится через brew).

В целом, все успешно инсталлировать получилось на Ubuntu Server 12.04, там и пакеты свежие, и ничего не вылетает.

Однако, из общения с разработчиками Symfony CMF в IRC мне стало ясно, что этот бэкенд-провайдер уже несколько месяцев как сломан, даже тесты для него отключены. Причина где-то на стороне команды Midgard2, хотя bergie пообещался таки починить.

IRC

У меня заставить работать Midgard2 PHPCR в составе Symfony CMF не получилось. Возможно, получится у кого-нибудь другого. Не сейчас, так потом.

Jackalope

Продолжая обыгрывать в названиях заячью тему (Jackrabbit, Jackalope), Jackalope предоставляет доступ к трем видам хранилищ данных:

  • это уже известный нам Apache Jackrabbit
  • Doctrine Database Abstraction Layer, который позволяет использовать поддерживаемые DBAL движки. Это в теории, на практике протестированы только MySQL, PostreSQL и SQLite (кто-то пользуется чем-то еще?).
  • MongoDВ (не обновлялось два года, вероятней всего сломано или неактуально)

Jackalope (и в частности jackalope-jackrabbit) достаточно стабилен и рекомендуется к использованию как наиболее полная в плане фич реализация PHPCR API. С ней и будем работать. Впрочем, phpcr-api-tests, проверяющие доступность и работоспособность PHPCR API, включены и для jackalope-doctine-dbal, возможно, со временем догонит.

Резюме по PHPCR

Итак, у нас есть (адаптированный под PHP) API для доступа к репозиториям контента, отвечающим стандарту JCR API. Для этого API разработаны несколько библиотек, абстрагирующих код приложения от хранилища данных.

Пока что должно возникнуть два главных вопроса, и на оба будет дан ответ:

Когда использовать PHPCR?

  • Когда нужно работать с иерархическими навигационными структурами
  • Когда у вас есть данные, связанные друг с другом
  • Когда нужна версионность данных

Когда НЕ использовать PHPCR?

Для строго структурированного контента и использования аггрегирующих запросов рекомендуется использовать реляционные базы данных. Например, в интернет-магазине описания товаров можно хранить в PHPCR, а заказы — в РСУБД.

PHPCR ODM

Спецификация это здорово, но API слишком абстрактный и неудобен для повседневного использования (ведь большинство привыкло к какой-нибудь ORM-системе). И тут на сцену выходит проект PHPCR ODM, который представляет собой связку PHPCR и Object Document Mapper.

Doctrine ORM, знакомая разработчикам, использующим SF2 (да и не только SF2), реализует паттерн Data mapper для доступа к данным, хранящимся в RDMBS.

ODM, подобно Doctrine ORM, использует Data mapper для полного отделения бизнес-логики от слоя хранения данных, которым в данном случае является репозиторий контента. Авторы честно признаются, что ODM навеян идеями Hibernate.

ODM сохраняет объекты в виде PHPCR-узлов, называя их документами. При этом, поскольку PHPCR уже независим от реализаций, не требует писать новый слой абстракции от базы данных (DBAL).

Что же такое документ в терминологии PHPCR ODM?

Документом является лаконичный PHP-класс, не реализующий какие-либо интерфейсы (верней, реализовать-то всегда можно, но сама библиотека этого не требует) и не унаследованный от каких-то базовых абстрактных классов. Такая сущность не должна иметь в себе методы с ключевым словом final, реализовывать методы clone() и wakeup(), или реализовывать, но делая это очень осторожно. Сама по себе сущность состоит из свойств, фиксируемых в хранилище. Поскольку ODM работает поверх библиотеки Doctrine Common, реализующей базовый функционал (аннотации, кэширование и автозагрузка классов), маппинг свойств в хранилище данных к свойствам класса производится знакомым всем путем — через аннотации в PHP-комментариях, либо YAML/XML-конфигах. У каждого документа есть заголовок (title) и содержимое (content). Все документы организованы в виде дерева и могут ссылаться на другие документы. Взгляните на пример документа:

namespace Demo;

use DoctrineODMPHPCRMappingAnnotations as PHPCRODM;

/**
 * @PHPCRODMDocument
 */
class MyDocument
{
    /**
     * @PHPCRODMId
     */
    private $id;
    /**
     * @PHPCRODMParentDocument
     */
    private $parent;
    /**
     * @PHPCRODMNodename
     */
    private $name;
    /**
     * @PHPCRODMChildren
     */
    private $children;
    /**
     * @PHPCRODMString
     */
    private $title;

    /**
     * @PHPCRODMString
     */
    private $content;

  // и еще горстка геттеров и сеттеров для записи и чтения свойств
}

Обратите внимание на то, что аннотации помимо привычных типов данных (например, String) могут также задавать тип ссылок на дочерние или родительские документы.

Для незнакомых с паттерном Data mapper может показаться, что такие классы немного похожи на Active record (привет, рельсовики и Yii-шники), однако таковыми они все равно не являются.

Как работать с таким документом?

require_once '../bootstrap.php';

// сначала находим корневой узел
$rootDocument = $documentManager->find(null, '/');

// создаем новый документ
$doc = new DemoDocument();
$doc->setParent($rootDocument);
$doc->setName('doc');
$doc->setTitle('My first document');
$doc->setContent('The document content');

// создаем второй, дочерний для первого
$childDocument = new DemoDocument();
$childDocument->setParent($doc);
$childDocument->setName('child');
$childDocument->setTitle('My child document');
$childDocument->setContent('The child document content');


// сообщаем менеджеру документов о том, какие документы у нас готовые для сохранения
$documentManager->persist($doc);
$documentManager->persist($childDocument);

// отправляем все изменения, вставки и т.д. в бэкенд
$documentManager->flush();

require_once '../bootstrap.php';

$doc = $documentManager->find(null, "/doc");
echo 'Found '.$doc->getId() ."n";
echo 'Title: '.$doc->getTitle()."n";
echo 'Content: '.$doc->getContent()."n";
foreach($doc->getChildren() as $child) {
    if ($child instanceof DemoDocument) {
        echo 'Has child '.$child->getId() . "n";
    } else {
        echo 'Unexpected child '.get_class($child)."n";
    }
}

// удаляем документ
$documentManager->remove($doc);

$documentManager->flush();

Небольшое замечание — в ORM привычно получать данные с помощью запросов. В ODM для этого надо использовать иерархию. Впрочем, можно и запросы делать, если сильно хочется.

В PHPCR ODM уже реализованы две очень важные функции — версионность и многоязычность. Начнем с первой.

Версионность в PHPCR бывает двух видов — simpleVersionable и versionable. Для простой версионности предусмотрены checkin/checkout-методы и линейная история изменений. Чекин создает новую версию узла и делает доступной только для чтения, чтобы что-то записать, нужно сделать чекаут.

Полная версионность вдобавок к этому поддерживает нелинейную историю версий (для которой хелпер-методов у PHPCR-ODM пока нет) и метки (которые планируется добавить, как только их начнет поддерживать Jackalope). Для каждого узла можно добавить метку к версии, но за всю историю каждого узла метка не может повторяться дважды (то есть, если хочется пометить другую версию, нужно сначала снять ее со старой версии).

Полная версионность соответсвует типу mix:versionable из PHPCR и позволяет создавать ветвление. К сожалению, PHPCR Version API не поддерживается PHPCR ODM целиком, для полноценной работы пока приходится работать с PHPCRVersionManager напрямую через PHPCR-сессию. Подробнее об этом тут и тут.

Имена версий генерируются PHPCR и не контролируются приложениям. Понятие коммит-месседжа отсутствует (просто пока не потребовалось). В любом случае, никто не мешает завести в документе поле под это дело.

Старые версии недоступы для модификации и сохранения изменений (исключение — при использовании методов restoreVersion() и removeVersion().

Чтобы включить поддержку версионности для какого-то документа, нужно явным образом указать это в аннотации:

/**
 * @Document(versionable="simple")
 */
class MyPersistentClass
{
    /** @VersionName */
    private $versionName;

    /** @VersionCreated */
    private $versionCreated;
}

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

$article = new Article();
$article->id = '/test';
$article->topic = 'Test';
$dm->persist($article);
$dm->flush();

// создаем слепок версии документа на основе текущего состояния
$dm->checkpoint($article);

$article->topic = 'Newvalue';
$dm->flush();

// получаем информацию о версиях
$versioninfos = $dm->getAllLinearVersions($article);
$firstVersion = reset($versioninfos);
// используем ее для получения слепка старой версии
$oldVersion = $dm->findVersionByName(null, $article->id, $firstVersion['name']);

echo $oldVersion->topic; // "Test"

// ищем новейшую версию
$article = $dm->find('/test');
echo $article->topic; // "Newvalue"

// устанавливаем старую версию в качестве последней
$dm->restoreVersion($oldVersion);

// документ обновился
echo $article->topic; // "Test"

// создаем еще одну версию, чтобы продемонстрировать удаление
$article->topic = 'Newvalue';
$dm->flush();
$dm->checkpoint($article);

// удаляем старую версию из истории (с последней так сделать не дадут)
$dm->removeVersion($oldVersion);

Теперь про многоязычность. Любое свойство документа можно пометить как переводимое. Несмотря на то, что при переводе в дереве будет склонирован документ целиком, поля, которым перевод не требуется, копироваться почем зря не будут. Каждый раз указывать язык для работы явно не требутся — достаточно один раз сказать DocumentManager, какой язык нам нужен, и дальше для всех вызовов типа find() и создания новых документов будет использоваться именно он. Удобно:

/**
 * @PHPCRODMDocument(translator="attribute")
 */
class MyPersistentClass
{
  /**
   * Текущая локаль документа
   * @Locale
   */
  private $locale;

  /**
   * Непереведенное свойство
   * @Date
   */
  private $publishDate;

  /**
   * Переведенное свойство
   * @String(translated=true)
   */
  private $topic;

  /**
   * Зависимая от языка картинка
   * @Binary(translated=true)
   */
  private $image;
}

И вот непосредственно работа с переводом полей:

// заранее загружаем DocumentManager (пример есть в документации)

$localePrefs = array(
    'en' => array('fr'),
    'fr' => array('en'),
);

$dm = new DoctrineODMPHPCRDocumentManager($session, $config);
$dm->setLocaleChooserStrategy(new LocaleChooser($localePrefs, 'en'));

// затем используем перевод:

$doc = new Article();
$doc->id = '/my_test_node';
$doc->author = 'John Doe';
$doc->topic = 'An interesting subject';
$doc->text = 'Lorem ipsum...';

// сохраняем документ на английском
$dm->persist($doc);
$dm->bindTranslation($doc, 'en');

// изменяем содержимое одного из полей и сохраняем на французском
$doc->topic = 'Un sujet intéressant';
$dm->bindTranslation($doc, 'fr');

// автоматически обновилась локаль
echo $doc->locale; // fr

// записываем изменения в PHPCR
$dm->flush();

// получаем документ на языке по умолчанию
// (английский в данном случае)
$doc = $dm->find(null, '/my_test_node');

// получаем документ на французском
$doc = $dm->findTranslation(null, '/my_test_node', 'fr');
$doc->title = 'nouveau';
$dm->flush(); // обновляем документ на французском, язык отслеживается менеджером документов

На текущий момент для всех ссылок, включая родительские и дочерние, предусмотрен фоллбек к языку по умолчанию, поскольку на время сессии можно отслеживать только один документ. Авторы пока не уверены, как поступить лучше, причем так же открытым пока висит вопрос обработки незавершенных документов (когда поля, заявленные в документе, на деле не существуют). Впрочем, время покажет, как эти проблемы будут решены.

Помимо этого в будущем улучшат логирование, кэширование результатов, начнут обновлять документацию (с этим пока очень скудно), возможно прикрутят поддержку Solr/ElasticSearch в связке с Doctrine DBAL и попробуют доделать реализацию для MongoDB. Сейчас разработчики поглядывают на следующую мажорную версию Jackrabbit (под кодовым именем Oak) и даже провели тесты на совместимость, однако первоочередной задачей все-таки является продвижение PHPCR и его включение в реальные приложения.

Подведем итог. При использовании ODM стек выглядит следующим образом:

  • PHP Content Repository с бэкендом в виде Jackalope или Midgard2 (данные хранятся в Jackrabbit или РСУБД)
  • PHPCR-ODM поверх Doctrine Common для удобной работы независимо от бэкенда
  • непосредственно код приложения.

Продолжение во второй части статьи.

Автор: waitekk

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js