Symfony Symfony, Twig обращение к объекту через репозиторий

bars80081

Новичок
Добрый день,

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

по мотивам Создание блога на Symfony 2.8 lts пытаюсь понять практическую пользу symfony.

Суть проблемы:
в статье описывается создание на Симфони блога. Присутствуют 2 сущности: блоги и комментарии. Несколько комментариев к одному блогу. К ним прилагаются репозитории. Соответственно, сущность комментария в коде описана примерно так:

PHP:
namespace Blogger\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;

/**
* @ORM\Entity(repositoryClass="Blogger\BlogBundle\Entity\Repository\CommentRepository")
* @ORM\Table(name="comment")
* @ORM\HasLifecycleCallbacks
*/
class Comment
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="integer")
     */
    protected $blog_id;

    /**
     * @ORM\ManyToOne(targetEntity="Blog", inversedBy="comments")
     * @ORM\JoinColumn(name="blog_id", referencedColumnName="id")
     */
    protected $blog;

    /**
     * @ORM\Column(type="text")
     */
    protected $comment;
то есть прямая связка с блогами посредством ORM. Также есть сущность Blog и репозиторий Blogger\BlogBundle\Entity\Repository\BlogRepository

В шаблоне twig при этом по выводу последних комментариев разных блогов прописано:
HTML:
    {% for comment in latestComments %}
        <article class="comment">
            <p class="small">{{ comment.user }} commented on {{ comment.blog.title }}</p>
            <p>{{ comment.comment }}</p>
        </article>
    {% endfor %}
благодаря чему помимо содержимого комментария на каждую запись вставляется и title блога.

Всё хорошо. Но...
если посмотреть на проходящие запросы к БД, то можно увидеть отвратную картину:
SQL:
...
SELECT * FROM blog t0 WHERE t0.id = 15
SELECT * FROM blog t0 WHERE t0.id = 13
SELECT * FROM blog t0 WHERE t0.id = 12
на каждый требуемый блог при первом запросе формируется запрос к БД. очень хорошо, что не на каждый запрос, а только при первом. далее, он их запоминает в репозитории и отдаёт уже без запросов к БД.

конечно же, я хочу от этого избавиться.
к примеру, если я напишу в контроллере страницы до вызова данного шаблона:
PHP:
        $idis = array();
        foreach($latestComments as $comment) {
            $idis[] = $comment->getBlogId();
        }
        $em->getRepository('BloggerBlogBundle:Blog')->findBy(array('id' => $idis));
то вместо этих штучных запросов мы получим всего один
SQL:
SELECT * FROM blog t0 WHERE t0.id IN (15,13,12)
как видно, он прекрасно обращается из шаблона к репозиторию.

в чём проблема?
проблема в том, что контроллер не должен отвечать за выборку данных. это дело репозитория или самой сущности. контроллеров может быть много, а репозиториев и сущностей по одному классу. я могу написать, чтобы репозиторий занимался накоплением искомых записей и при требовании осуществлял запрос сразу всех, но я не могу никак заставить шаблон twigа пройти через репозиторий или форсировать запуск поиска в репозитории из сущности.
запрос от ORM в репозиторий не проходит через метод ->find() или подобные. вызовов из сущности явным способом тоже нет. мало того, из сущности я не могу дотянуть ни до доктрины, ни до EntityManager (хотя в принципе понимаю, что и не должно вообще-то быть такого). в методе Comment::getBlog() блога может ещё не существовать, но при первом обращении внутри к $this->blog->smth; он уже оперирует объектом с заполненными данными, то есть осуществил запрос.

копаюсь в классах Симфони и документации уже несколько дней, глаза вытекают.

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

спасибо!
 

bars80081

Новичок
достигнут некоторый промежуточный результат, но пока что в рамках всё той же магии ORM

PHP:
/**
 * @ORM\Entity(repositoryClass="Blogger\BlogBundle\Entity\Repository\CommentRepository")
 * @ORM\Table(name="comment")
 * @ORM\HasLifecycleCallbacks
 */
class Comment
{
    /**
     * @ORM\ManyToOne(targetEntity="Blog", inversedBy="comments", fetch="EAGER")
     * @ORM\JoinColumn(name="blog_id", referencedColumnName="id")
     */
    protected $blog;
если вставить fetch="EAGER", то вместо трёх запросов при вызове конкретного блога, будет реализован всего один сразу после осуществления запроса выборки последних комментариев.

задача при этом фактически не выполняется и даже имеет возможность развития в перенапряжение ресурса. ведь не всегда при получении комментариев требуется получить блоги.

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

A1x

Новичок
в CommentRepository сделать метод findLatestComments() в котором вытащить эти каменты запросом DQL с JOIN Blog, не?
 

bars80081

Новичок
нет, joinы только утяжелят фон запросов. требуется именно запуск поиска накопленных значений.
вообще, это классический lazy load, однако, к примеру, в ORM lazy понимают как-то по своему. так как альтернативой fetch="EAGER" есть LAZY и EXTRA LAZY. где LAZY выполняет отложенную загрузку (только когда она понадобится), но только всего одной записи.

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

допустим есть форум, где на странице в основной полосе приводятся топики, а также пишется автор топика и автор последнего сообщения. помимо основной полосы есть включения дополнительных блоков: последние сообщения всего по форуму, топ-лист авторов за всё время, топ-лист авторов за неделю, список награждённых, список пользователей у которых сегодня день рождения и т.д. и т.п.
задействовано несколько таблиц, в которых хранится id пользователей (topics.creator, topics.last_post_user, posts.creator, awards.user, users.id и т.д.). при этом на стадии сбора данных существуют только id пользователей, а вот на стадии сборки страницы из шаблонов требуются уже не id пользователей, а имя, день рождения, число наград и т.п. Сборка из шаблонов может происходить в самом конце, когда забор всех данных уже произведён.
очевидно, что не имеет смысла джойнить каждый запрос к таблице данных с таблицей пользователей, так как это утяжелит каждый запрос в отдельности. также не имеет смысла обращаться к таблице пользователей сразу после запроса к таблице в каждом модуле в отдельности, так как мы получим кучу идентичных запросов. самое логичное - комплексный lazy load - один запрос по накопленному массиву идентификаторов в тот момент, когда по настоящему потребовались данные по первому пользователю.
минимум запросов, минимум нагрузки.
сделать это в самописном коде не сложно. но если уж мы начинаем использовать такой мощный инструмент, как Симфони, то хотелось бы и в нём.
 

Yoskaldyr

"Спамер"
Партнер клуба
форум может быть довольно нагруженным проектом и использовать симфони для read моделей может совсем не айс. да и вообще все ормы лучше идут для write моделей, но это имхо.
 

bars80081

Новичок
ну, в данном случае есть желание использовать Симфони. да и форум приведён только для наглядного примера демонстрации этапов выборки.
вопрос, даст ли Симфони организовать процесс таким образом
 

bars80081

Новичок
таки решение найдено.

убираем вредный EAGER, а в сущность Comment добавляем злую магию на событие, плюс дополняем метод getBlog():
PHP:
/**
 * @ORM\Entity(repositoryClass="Blogger\BlogBundle\Entity\Repository\CommentRepository")
 * @ORM\Table(name="comment")
 * @ORM\HasLifecycleCallbacks
 */
class Comment
{
    /**
     * @ORM\PostLoad
     * @ORM\PostPersist
     */
    public function fetchEntityManager(LifecycleEventArgs $args)
    {
        if(is_null($this->em)) {
            $this->em = $args->getEntityManager();
        }
    }
    /**
     * Get blog
     *
     * @return \Blogger\BlogBundle\Entity\Blog
     */
    public function getBlog()
    {
        if($this->em && !empty($this->blog_id)) {
            return $this->em->getRepository('BloggerBlogBundle:Blog')->find($this->blog_id);
        }

        return $this->blog;
    }
...
, а также дополняем метод забора данных в CommentRepository сохранением идентификаторов:
PHP:
/**
 * CommentRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class CommentRepository extends EntityRepository
{
    public function getLatestComments($limit = 10)
    {
        $qb = $this->createQueryBuilder('c')
                    ->select('c')
                    ->addOrderBy('c.id', 'DESC');

        if (false === is_null($limit))
            $qb->setMaxResults($limit);

        $results = $qb->getQuery()->getResult();
        // вставка
        foreach($results as $comment) {
            $this->_em->getRepository('BloggerBlogBundle:Blog')->setOne($comment->getBlogId());
        }
        // конец вставки
        return $results;
    }
, где метод setOne() - самописный метод из группы методов в классе репозитория, позволяющий включить аргумент в очередь. Далее там в BlogRepository переписывание наследуемых методов типа find(), findAll(). findBy() и т.п., которые позволяют запросить всю накопленную очередь.

по факту остаётся методику работы с очередью вынести в некое центральное место (промежуточный класс между BlogRepository и \Doctrine\ORM\EntityRepository), чтобы не пришлось писать в каждом репозитории, а достаточно было от него унаследоваться.

соответственно, теперь можно выслушать недовольство, если подобный обвес события является чем-то очень плохим.
дело в том, что на stackoverflow данная методика названа VERY BAD PRACTICE. но не слишком объясняя почему
 
Сверху