Несколько «плюшек» для Symfony 2 && Doctrine

в 14:30, , рубрики: doctrine, Events, symfony2, Песочница, метки: , ,

В этой статье пойдет речь о том, как можно решить некоторые проблемы в 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. В таких ситуациях мы можем отключить все логеры, которые использует доктрина (менеджер), или же полностью переопределить, чтобы контролировать только то, что нужно. Сам же логгер висит в конфигурации доктрины.

Пример полного отключения:

PHP

$container->get('doctrine')
    ->getManager()
    ->getConnection()
    ->getConfiguration()
    ->setSQLLogger(null);
    // Здесь можно внедрить свой логгер

Configuration

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
Но если Вы не имеете полный доступ к системе (к примеру хостинг) и Вас уже достала эта проблема, то можно разделить систему запуска на dev и console. Делается это следующим образом:

  1. Создаем файлик app/config/config_console.yml, в который импортируем конфиг config_dev.yml
  2. В файле app/console по дефоулту выставляем не dev а console
  3. Корректируем файл app/AppKernel.php в том месте, где подключаются дополнительные бандлы (идет проверка environment) и добавляем еще console

Теперь при запуске app/console система будет работать под совсем другим environment, что и не будет мешать dev-у
Внимание: речь идет только если система находиться в dev режиме!

P.S. Коды писались для примера, а не для работы на прод системах. Использовалась Symfony 2.2.
P.S.S. Прошу сильно не пинать… Мой первый пост. Спасибо за внимание!

Автор: ZhukV

Источник

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


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