В этой статье пойдет речь о том, как можно решить некоторые проблемы в Symfony 2 и Doctrine, используя базовые компоненты из коробки, а именно:
- Внедрение сервиса в модель
- Сохранение истории изменения
- Отключение SQLLogger и чистка кеша
- Разделение environment (dev — console)
Думаю многие кто работал с Symfony 2 сталкивался с проблемой внедрения сервис контейнера в модель (Entity) или же какого-то другого компонента (сервиса). Прежде чем Вы узнаете как можно это сделать, подумайте, нужно ли Вам это? Можно создать определенный сервис (утилиту) и сделать уже там все то что необходимо. Ну если Вы решили, что это нужно, тогда:
Плюшка №1 Внедрение сервиса в модель:
Есть несколько способов внедрения, и зачастую все выбирают самый простой.
Вариант №1
Взять и «тупо» вызвать кернел с глобальной области видимости
class MyEntity
{
// ... properties
// ... methods
public function someAction()
{
global $kernel;
return $kernel->getContainer()
->.....
}
}
Как по мне способ очень простой, но и очень дырявый, так как невозможно проверить существование той же переменной…
Вариант №2
Здесь нужно будет немного «попотеть» для стой простой функции, но это решение будет намного лучше первого и гарантирует полную работоспособность. Здесь будут использоваться евенты доктрины, которые разрешают нам делать все что угодно во время загрузки/сохранения модели.
Для начала нам необходимо будет создать либо Event либо Subscriber для Doctrine, которые внедрит необходимый компонент во время загрузки модели (postLoad). И все это дело нам нужно будет создать как сервис, чтобы можно было передать любой сервис.
Создаем Subscriber для Doctrine:
namespace AcmeDemoBundleEventListener;
use DoctrineCommonEventSubscriber;
use DoctrineORMEventLifecycleEventArgs;
use SymfonyComponentDependencyInjectionContainerInterface;
use AcmeDemoBundleEntityMyEntity;
class MyEntitySubscriber implements EventSubscriber
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function postLoad(LifecycleEventArgs $event)
{
$entity = $event->getEntity();
// Ну вот, это наша модель, можем пихать с контейнера все что угодно,
// хоть даже сам контейнер
if ($entity instanceof MyEntity) {
$entity-> setMyComponent($this->container->get('....'));
}
}
/**
* Определяем, какие именно эвенты мы будем использовать
* Внимание: само название евента - и есть название
* функции, которая будет вызываться
*/
public function getSubscribedEvents()
{
return array(
'postLoad'
);
}
}
Вот как выглядит новая модель:
class MyEntity
{
// … properties
protected $myComponent;
// … methods
public function setMyComponent($myComponent)
{
$this->myComponent = $myComponent;
}
public function someAction()
{
return $this->myComponent
->....
}
}
Регистрируем как сервис
<service id="my_entity.doctrine.subscriber" class="AcmeDemoBundleEventListenerMyEntitySubscriber">
<!-- Тег необходим для того, чтобы доктрина приняла этот subscriber -->
<tag name="doctrine.event_subscriber" />
<argument type="service" id="service_container" />
</service>
Ну вот и все, при загрузке модели, система проверит, является ли эта модель необходимой, и если да, то «втулит» необходимый компонент в переменную myComponent используя отдельный сеттер.
Заключение: пытайтесь избежать таких дел, так как модели служат только для чтения/записи в БД.
Плюшка №2 Сохранения истории:
Иногда приходиться сохранить всю историю изменения некоторой модели, ну к примеру — история изменения записи для дальнейшей возможности отката. В данной ситуации очень сильно помогут евенты самой доктрины, а именно onFlush
Предположим у нас есть модель «новость», за которой нам нужно будет следить, а именно за полем title:
class News
{
protected $title;
// ... properties
// ... methods
}
Ну и в догонку необходимо будет создать еще одну модель, которая как раз и будет контролировать изменения:
class NewsHistory
{
protected $title;
// ... properties
// ... methods
}
На самом евенте нужно будет проверить, изменялось ли это поле. В этом поможет UnitOfWork, который тщательно следит за всем моделями в определенном менеджере.
namespace AcmeDemoBundleEventListener;
use DoctrineCommonEventSubscriber;
use DoctrineORMEventOnFlushEventArgs;
use SymfonyComponentDependencyInjectionContainerInterface;
use AcmeDemoBundleEntityNews;
use AcmeDemoBundleEntityNewsHistory;
class NewsSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $event)
{
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
// Проходим по всем ентити, которые должны обновиться
foreach ($uow->getScheduledEntityUpdates() as $entity) {
// Это наша новость
if ($entity instanceof News) {
// Достаем массив всех полей, которые изменились
$changeSet = $uow->getEntityChangeSet($entity);
// Присутствует ли наше поле title?
if (isset($changeSet['title'])) {
// Достаем старое и новое значение
list ($oldTitle, $newTitle) = $changeSet['title'];
// Создаем entity для сохранения истории
$newsHistory = new NewsHistory;
$newsHistory->setTitle($oldTitle);
// Тыкаем новую модель в менеджер
$em->persist($newsHistory);
// ВНИМАНИЕ!!!
// Система сразу не распознает эту модель как ту, которая
// должна сохраниться, по причине того, что flush менеджера
// уже начался, который как раз и определяет, что нужно
// обновить, а что вставить.
// На помощь нам приходит UnitOfWork, которому можем четко указать,
// что вот эту модель необходимо сохранить
// Для начала достаем ClassMetadata для объекта
// Здесь храниться вся "мета" информация о данной модели
$classMetadata = $em->getClassMetadata(get_class($newsHistory));
// Ну и теперь "тыкаем" в UnitOfWork, что эту модель нужно вставить
$uow->computeChangeSet($classMetadata, $newsHistory);
}
}
}
}
public function getSubscribedEvents()
{
return array(
'onFlush'
);
}
}
Ну и как всегда, нужно зарегистрировать как сервис, указывая, что это subscriber для доктрины:
<service id="news.doctrine.subscriber" class="AcmeDemoBundleEventListenerNewsSubscriber">
<tag name="doctrine.event_subscriber" />
</service>
UnitOfWork — это так называемое Api, при помощи которого Вы можете делать любые махинации с моделями.
Таким же образом, можно контролировать создание новых моделей и удаление старых.
Плюшка №3 Отключение SQL логгера, чистка кеша в Doctrine:
Иногда есть необходимость запустить команды (с консоли) которые делают очень много запросов к БД. Хорошим примером может послужить перенос сайта с какой-то CMS или фраемворка на Symfony 2 при наличии более 100 000 записей в БД. Если запустить какую-то команду на Симфони с консоли, то по умолчанию, система запустится в режиме dev и включенным дебагом, в котором доктрина сохраняет все запросы для последующего дебага. В результате чего постоянно увеличивается память выделенная для скрипта, что в последствии влияет на скорость и может выбросить критическую ошибку memory limit. В таких ситуациях мы можем отключить все логеры, которые использует доктрина (менеджер), или же полностью переопределить, чтобы контролировать только то, что нужно. Сам же логгер висит в конфигурации доктрины.
Пример полного отключения:
$container->get('doctrine')
->getManager()
->getConnection()
->getConfiguration()
->setSQLLogger(null);
// Здесь можно внедрить свой логгер
doctrine:
dbal:
connections:
default:
logging: false
Также следует не забывать, что все модели, которые были загружены с БД сохраняются в менеджере до тех пор пока не будет вызван unset или же сброшен «статический» кеш. Это было разработано для того, чтобы не делать постоянно запросы на выборку одной и той же модели. В результате, при таких больших скриптах (которые делают множество операций с моделями) необходимо периодически чистить кеш:
$container->get('doctrine')->getManager()->clear();
Плюшка №4 Разделение environments для консоли:
При поднятии новой сборки Симфони, всегда сталкиваемся с проблемой некоторых папок (Permission denied): app/cache и app/logs. Эта проблема возникает тогда, когда Вы уже запустили сайт с браузера и пытаетесь сделать что-то в консоли. Проблема в том, что сам веб сервер работает под другим пользователем.
Те, у кого есть полный доступ к серверу (root), этого делать не нужно, посмотрите лучше: http://symfony.com/doc/current/book/installation.html#configuration-and-setup
Но если Вы не имеете полный доступ к системе (к примеру
- Создаем файлик app/config/config_console.yml, в который импортируем конфиг config_dev.yml
- В файле app/console по дефоулту выставляем не dev а console
- Корректируем файл app/AppKernel.php в том месте, где подключаются дополнительные бандлы (идет проверка environment) и добавляем еще console
Теперь при запуске app/console система будет работать под совсем другим environment, что и не будет мешать dev-у
Внимание: речь идет только если система находиться в dev режиме!
P.S. Коды писались для примера, а не для работы на прод системах. Использовалась Symfony 2.2.
P.S.S. Прошу сильно не пинать… Мой первый пост. Спасибо за внимание!
Автор: ZhukV