N
N
Nikolai Egorov2019-02-25 19:34:58
symfony
Nikolai Egorov, 2019-02-25 19:34:58

What is the best way to implement the code for processing an AJAX request from different entities to the same controller?

Help me figure out the necessary code structure. There is a clear understanding of what I have done ... crooked. It works, but the code is cumbersome and repetitive.
I have many entities with relationships. To manage these links in the admin, I use tetranz/select2entity-bundle . Accordingly, in the form class, a field with a link is described with an indication of the Select2EntityType type. One of the parameters for this field type is remote_route, which specifies the route for processing the Ajax request, which returns a list of potential entities to create a connection.
Next, I will give an example of a specific connection and code. There are such entities as Category (category), Tag (tag) and Faq (question / answer). Both Category and Questions have a relationship with the Tag entity. When creating/editing a connection with the Tag entity, the search is performed by the name field of the Tag entity (by the tag name).
The code of the CategoryType and FaqType form, as well as the controller that processes the Ajax request from all entities that have a connection with the tag (there are more of them, not only Category and 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);
    }
}

Actually the questions are. I will set points in order to articulate the disturbing points more clearly. The first one is easy to formulate.
  • Question 1. There are several entities with which Tag has a relationship (category, faq, article, etc.), and all these entities make a request to the same TagController::search() controller method. Is it correct? I think yes, but there is another point...

I need to give a list of Tags, excluding from it those that are already assigned to this entity, and for this I need to know what kind of entity is being edited (its class) and what its id is.
For example, we edit the list of tags for a category. To do this, I pass 2 additional parameters with the AJAX request (this can be seen in the code of both forms): entityClass and entityId.
'remote_params' => [
                    'entityClass' => urlencode(Faq::class),
                    'entityId' => $options['data']->getId(),
                ]

Next, I get the name of the Entity class in the controller:
$entityClass = urldecode($request->query->get('entityClass'));

And I get the corresponding repository:
$entityRepository = $this->getDoctrine()->getRepository($entityClass);

  • Question 2. Is it correct to pass and receive the name of the Entity class in my case? I can’t load (inject into the controller) all the repositories at once, so I determine the desired repository by the class name.

Then I do what I did everything for - I get an editable category (a category because there is an example with a category, although there may be another entity) and I get the tags assigned to it:
$entity = $entityRepository->findOneBy(['id' => $entityId]);
/** @var ArrayCollection|Tag[] */
$conditions['excludeTags'] = $entity->getTags();

Next, $conditions, along with the search string, is passed to the tag repository to fetch tags.
$foundTags = $tagRepository->findBySearchQuery($query, $limit, $conditions);
I will not give the entire code of the method that generates a query to the database, only the important part - this part just adds the condition 'NOT IN (id, id, id, id)' to the query to the database:
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));
            }
        }

  • Question 3. In fact, when building a query to the database, I need not Tag objects, but their id's. Therefore it is necessary foreach'em to get their list. Can it be more correct to pass through $conditions['excludeTags'] not a collection of entity objects, but simply an array with id's?
  • Question 4. If yes, then what is the best way to get this array?
    1. Move foreach ($conditions['excludeTags'] as $tag) to the controller?
    2. Or do not request a category in the controller at all, and accordingly do not receive assigned tags from the category ...
    $entity = $entityRepository->findOneBy(['id' => $entityId]);
    and make a separate method in the tag repository (TagRepository), like getAssignedTagsForEntity($entityType, $entityId), which should return ... what ... just id's or all the same tag objects (Tag)? True, I have not yet thought about how to implement binding inside this method - apparently through switch ($ entityType).

I understand that the end of this post came out chaotic, but I tried my best to explain what confuses me in this situation. And the reality is that there is a similar controller method with a route like "admin_ХХХ_ajax_searching" in almost every controller - article, category, gallery, tag and there will be others, I'm sure. And they are all the same type, they confuse everyone with the questions that I described above. I want to rewrite, but I don’t know where to start, and how to do it right.
In general, the main question, generalizing so to speak, is set in the topic topic - "How to implement the processing of an AJAX request from different entities to one controller?". Perhaps the way I did it is fundamentally wrong ...
Help please :)
Thank you!

Answer the question

In order to leave comments, you need to log in

1 answer(s)
F
Flying, 2019-02-25
@nickicool

Depending on the real needs of your application, you can look towards using entities inheritance . For example, perhaps your "category, faq, article" are special cases of a common logical entity Content, in which case it would be possible to request a repository specifically for the base entity and then you will not have separation of the logic of working with the same tags.
In addition, nothing prevents you from moving the code for working with tags into a separate service class (let's call it TagSearchService), for each type "category, faq, article" separate actions in their controllers that will refer to the service class with different (and in this case, beforehand known) parameters. That is, in other words, in some CategoryController::search()you could call TagSearchServiceand pass to itCategory::classinstead of relying on input from outside. If you switch to such a scheme, then different classes for form elements ( CategoryTypeand FaqTypein your example) are also naturally replaced by one class (any one TagSearchType). there is no difference between them. In addition, it will not be necessary to pass the class name from the outside - in my opinion this is a bad idea in any case. If we develop this idea further, then an interface for entities that can have tags
logically emerges . TaggableInterfaceThis naturally leads to the possibility in the compiler pass to collect a list of such entities and pass them to TagSearchService. Might be needed for some purposes :)
Next, about filtering tags. Still, when building queries, we are talking about DQL, so there is a non-zero chance (although I can’t prove it, I have to try) that you don’t need to pull out the id from the tags, it’s enough to pass the array of the entities themselves to notIn(). If this is not the case, the code can be rewritten using array_map(), perhaps this will make it clearer. It also findOneBy(['id' => $entityId])obviously changes to EntityManager::find()something that looks a little simpler.
As for the idea of ​​pulling exactly the id tags - it hardly makes any special sense unless you have a very loaded application.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question