Как правильнее реализовать код обработки AJAX запроса от разных сущьностей к одному контроллеру?

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

У меня много сущностей с взаимосвязями. Чтобы управлять этими связями в админке я использую tetranz/select2entity-bundle. Соответственно в классе формы, поле со связью описано с указанием типа Select2EntityType. Одним из параметров у этого типа поля, является remote_route, который задает роут для обработки Ajax запроса, который возвращает список потенциальных сущностей для создания связи.

Дальше я приведу пример конкретной связи и код. Есть такие сущности, как Category (категория), Tag (тег) и Faq (вопрос/ответ). И Категория и Вопросы имеют связь с сущьностью Тег. При создании/редактировании связи c сущностью Tag поиск идет по полю имени сущности Tag (по имени тега).

Код формы CategoryType и FaqType, а так же контроллера, который и обрабатывает Ajax запрос от всех сущностей которые имеют связь с тегом (их больше, не только Category и Faq):

class CategoryType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
            ->add('tags', Select2EntityType::class, [
                'multiple' => true,
                'remote_route' => 'admin_tag_ajax_searching',
                'class' => Tag::class,
                'remote_params' => [
                    'entityClass' => urlencode(Category::class),
                    'entityId' => $options['data']->getId(),
                ],
            ]);
    }
}

class FaqType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
            ->add('tags', Select2EntityType::class, [
                'multiple' => true,
                'remote_route' => 'admin_tag_ajax_searching',
                'class' => Tag::class,
                'remote_params' => [
                    'entityClass' => urlencode(Faq::class),
                    'entityId' => $options['data']->getId(),
                ],
            ]);
    }
}

class TagController extends Controller
{
    /**
     * @Route("/tag_ajax_searching", methods={"GET"}, name="admin_tag_ajax_searching")
     */
    public function search(Request $request, TagRepository $tagRepository): Response
    {
        $query = $request->query->get('q', '');
        $limit = $request->query->get('limit', 20);
        $entityId = $request->query->get('entityId');
        $entityClass = urldecode($request->query->get('entityClass'));

        $conditions = [];
        // исключить из поиска уже назначенные Теги
        if ($entityId) {
            $entityRepository = $this->getDoctrine()->getRepository($entityClass);
            $entity = $entityRepository->findOneBy(['id' => $entityId]);
            /** @var ArrayCollection|Tag[] */
            $conditions['excludeTags'] = $entity->getTags();
        }

        $foundTags = $tagRepository->findBySearchQuery($query, $limit, $conditions);

        $results = [];
        foreach ($foundTags as $tag) {
            $results[] = [
                'id' => htmlspecialchars($tag->getId()),
                'text' => htmlspecialchars($tag->getTitle()),
            ];
        }

        return $this->json($results);
    }
}


Собственно вопросы такие. Задам пунктами, чтобы четче сформулировать беспокоящие моменты. Первый сформулировать просто
  • Вопрос 1. Сущностей, с которыми Tag имеет связь - несколько (category, faq, article и т.д.), и все эти сущности делают запрос к одному методу контроллера TagController::search(). Правильно ли это? Я думаю да, но есть следующий момент...


Мне нужно отдать список Тегов исключив из него те, которые уже назначены у данной сущности, а для этого мне нужно знать, что за сущность редактируется (ее класс) и какой у нее id.

Например, редактируем список тегов у категории. Для этого я передаю с AJAX запросом 2 дополнительных параметра (в коде обеих форм это видно): entityClass и entityId.
'remote_params' => [
                    'entityClass' => urlencode(Faq::class),
                    'entityId' => $options['data']->getId(),
                ]

Дальше я получаю имя класса Entity в контроллере:
$entityClass = urldecode($request->query->get('entityClass'));

И получаю соответствующий репозиторий:
$entityRepository = $this->getDoctrine()->getRepository($entityClass);

  • Вопрос 2. Правильно ли так передавать и получать имя класса Entity в моем случае? Я же не могу подгружать (инектить в контроллер) сразу все репозитории, поэтому определяю нужный репозиторий по имени класса.


Дальше уже делаю то, ради чего все делалось - получаю редактируемую категорию (категорию потому что пример с категорией, хотя там может быть и другая сущность) и достаю назначенные ей теги:
$entity = $entityRepository->findOneBy(['id' => $entityId]);
/** @var ArrayCollection|Tag[] */
$conditions['excludeTags'] = $entity->getTags();

Дальше $conditions, вместе со строкой поиска, передается в репозиторий тегов для выборки тегов.
$foundTags = $tagRepository->findBySearchQuery($query, $limit, $conditions);
Весь код метода, который выполняет генерацию запроса к БД приводить не буду, только важную часть - эта часть как раз добавляет в запрос к БД условие 'NOT IN(id, id, id, id)':
if (isset($conditions['excludeTags'])) {
            /** @var $tag Tag */
            foreach ($conditions['excludeTags'] as $tag) {
                $ids[] = $tag->getId();
            }

            if (isset($ids)) {
                $qb->andWhere($qb->expr()->notIn('tag.id', $ids));
            }
        }

  • Вопрос 3. Фактически, при построении запроса к БД мне нужны не объекты Tag, а их id'шники. Поэтому приходится foreach'ем доставать их список. Может правильнее передавать через $conditions['excludeTags'] не коллекцию объектов сущностей, а просто массив с id'шниками?
  • Вопрос 4. Если да, то тогда как лучше получать этот массив?

    1. Вынести foreach ($conditions['excludeTags'] as $tag) в контроллер?
    2. Или вообще в контроллере не запрашивать категорию, а у категории соответственно не получать назначенные теги...
    $entity = $entityRepository->findOneBy(['id' => $entityId]);
    а сделать в репозитории тегов (TagRepository) отдельный метод, типа getAssignedTagsForEntity($entityType, $entityId), который должен возвращать... что... просто id'шники или все таки объекты тегов (Tag)? Правда как реализовать связывание внутри этого метода я пока не продумывал - видимо через switch ($entityType).


Понимаю, что конец данного поста сумбурный вышел, но я старался как мог объяснить то, что меня смущает в данной ситуации. А реальность такова, что подобный метод контроллера, с роутом типа "admin_ХХХ_ajax_searching" есть практически в каждом контроллере - article, category, gallery, tag и будут еще другие, уверен. И все они однотипны, все смущают теми вопросами, что я описал выше. Хочу переписать, но не знаю с чего начать, и как правильнее.

Вообще, главный вопрос, обобщающий так сказать, задан в теме топика - "Как реализовать обработку AJAX запроса от разных сущностей к одному контроллеру?". Возможно то, как я это сделал - вообще в корне неверно...

Помогите пожалуйста :)

Спасибо!
  • Вопрос задан
  • 127 просмотров
Решения вопроса 1
@Flying
В зависимости от реальных потребностей вашего приложения можно посмотреть в сторону использования наследования entities. К примеру возможно ваши "category, faq, article" являются частными случаями общей логической сущности Content, в этом случае можно было бы запрашивать репозиторий именно для базовой entity и дальше у вас не будет разделения логики работы с теми же тегами.

Кроме того ничто не мешает вам вынести код работы с тегами в отдельный сервисный класс (назовём его TagSearchService), сделать для каждого типа "category, faq, article" отдельные actions в их контроллерах которые будут обращаться к сервисному классу с разными (и в этом случае заранее известными) параметрами. Т.е., проще говоря, в каком-нибудь CategoryController::search() вы могли бы вызвать TagSearchService и передавать ему Category::class вместо того чтобы полагаться на приходящие извне данные. Если переходить на такую схему - то разные классы для элементов форм (CategoryType и FaqType в вашем примере) тоже естественным образом заменяются на один класс (какой-нибудь TagSearchType) т.к. между ними не остаётся различий. Кроме того не нужно будет передавать извне имя класса - по-моему это плохая идея в любом случае.

Если развивать эту идею дальше - то логично вырисовывается интерфейс TaggableInterface для entities которые могут иметь теги. Это естественным образом приводит к возможности в compiler pass собрать список таких entities и передать их в TagSearchService. Возможно потребуется для каких-то целей :)

Далее, по поводу фильтрации тегов. Всё-таки при построении запросов мы ведём речь о DQL, так что есть ненулевой шанс (хотя доказать это я и не могу, надо пробовать) что вытаскивать id из тегов не нужно, достаточно передать массив самих entities в notIn(). Если же это и не так - код можно переписать с использованием array_map(), возможно это сделает его понятнее. Также findOneBy(['id' => $entityId]) очевидным образом меняется на EntityManager::find() что несколько проще выглядит.

Насчёт идеи вытягивать именно id тегов - вряд ли в этом есть особый смысл если только у вас не очень нагруженное приложение.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы