ux
ux copied to clipboard
[Autocomplete] Document how to pass related entity
How can I use Autocomplete to filter based on some underlying data?
That is, I only want to autocomplete answers from a specific question.
With a regular choice field, I can do something like
$question = $options['data'];
...
'query_builder' => function($answerRepo) use ($question) {
return $answerRepo->createQueryBuilder('a')
where('a.question =: question')->setParameter('question', $question);
I tried overriding query_builder in the form that calls AnswerAutoCompleteField, but that didn't work. I can't figure out how to pass data to the AnswerAutoCompleteField class to use in the query builder.
Hmm. This is actually a bigger problem than just documentation - it's a case I hadn't considered.
The tricky thing is that the AJAX endpoint doesn't know anything about your current form's data. You basically just make an AJAX call to /autocomplete/answer_autocomplete and that grabs the query_builder option from your AnswerAutoCompleteField... but in this situation, the $options['data'] will be empty, as we're just grabbing AN instance or your field, but it doesn't have access to your form data.
So, to allow this (which seems reasonable), we need to be able to pass some extra information via query parameters to the Ajax endpoint, then supply this to you so you can modify the query. Possible API for this:
// AnswerAutoCompleteField.php
public function buildForm(...)
{
// ...
'query_context' => function() {
return ['question' => $options['data']->getId()];
},
'query_builder' => function($answerRepo, array $context) {
return $answerRepo->createQueryBuilder('a')
->andWhere('a.question := question')->setParameter('question', $context['question']);
}
}
The query_context would result in the Ajax URL being something like: /autocomplete/answer_autocomplete?question=3&query=foo. One tricky thing (not sure if it's possible yet) is that we would need to replace the normal DoctrineType query_builder normalizer with our own so that we could invoke the callable with the new $context argument.
Additionally, we should:
A) On the Ajax endpoint, if the original query_context has a question key, then guarantee there is a question query parameter available. If there is not, 400 error.
B) The $context part of the URL (at the very least) needs to be signed so that "bad users" can't just modify and start auto-completion answers from any question.
Although the documentation says to avoid passing anything in the 3rd argument, it seems natural to be able to do so -- that is, to pass the querybuilder with the custom filtering. Obviously, it's not that simple.
Adding the filtering makes me wish we could leverage API Platform, because of how powerful it is once it's configured. And it returns json, so can be called directly from stimulus.
So perhaps there could be a new UX component that is an autocomplete based on a api platform uri? Just brainstorming.
I'll add my two cents here, although the comment might as well go under #405. (Maybe I shouldn't have started that one, sorry).
The architectural hurdle here is the lost context at the AJAX endpoint. The solution is to write your own endpoint and do all the heavy lifting.
Does it show an opportunity for a middle-ground approach, where the UX component would provide us with a service that
- we autowire where appropriate (most often the custom endpoint's controller action),
- give it a custom-made
EntityAutocompleterInterfaceobject (see pseudo code below) - and the service would take care of the rest - finding data, building a response
- we would just return the response generated by that service
Pseudo code of the autocompleter:
class AnswerAutocompleter implements EntityAutocompleterInterface
{
public function __construct(private readonly Question $question) {
// ...
}
public function createFilteredQueryBuilder(EntityRepository $repository, string $query): QueryBuilder
{
return $repository
// ...
->andWhere('question = :question')
->setParameter('question', $this->question->getId())
;
}
}
The autocompleter's controller action
#[Route('/autocomplete/{question}')]
controllerAction(Question $question, AutocompleteService $svc) {
return $svc->doTheHeavyLifting(new AnswerAutocompleter($question));
}
Sounds like an idea?
For my part, when I use the custom vision with a leftJoin, I systematically get the error
Compile Error: Cannot declare class App\Entity\User, because the name is already in use
Is this related to the same problem?
<?php
// ContactsMemberAutocompleter.php
namespace App\Autocompleter;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityRepository;
use App\Entity\ContactsMember;
use Symfony\Component\Security\Core\Security;
use Symfony\UX\Autocomplete\EntityAutocompleterInterface;
use Symfony\Bundle\SecurityBundle\Security as SecurityUser;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('ux.entity_autocompleter', attributes: ['alias' => 'cm'])]
class ContactsMemberAutocompleter implements EntityAutocompleterInterface
{
public function __construct(private SecurityUser $securityUser)
{
$this->securityUser = $securityUser;
}
public function getEntityClass(): string
{
return ContactsMember::class;
}
public function createFilteredQueryBuilder(EntityRepository $repository, string $query): QueryBuilder
{
$user = $this->securityUser->getUser();
$q = $repository
// the alias "food" can be anything
->createQueryBuilder('ContactsMember')
->select('(ContactsMember.contactUser) as id, (User.lastName) as lastName, (User.firstName) as firstName, (User.email) as email, (User.alias) as alias')
->leftJoin('App\\Entity\\User', 'User', 'WITH', 'ContactsMember.contactUser = User.id')
->where('ContactsMember.user = ' . $user)
->andWhere('ContactsMember.contactUser != ' . $user)
->andWhere('ContactsMember.visible = 1')
->andWhere('
User.lastName LIKE :search OR
User.firstName LIKE :search OR
User.email LIKE :search OR
User.alias LIKE :search
')
->setParameter('search', '%'.$query.'%')
// maybe do some custom filtering in all cases
//->andWhere('food.isHealthy = :isHealthy')
//->setParameter('isHealthy', true)
;
return $q;
}
public function getLabel(object $entity): string
{
return $entity->getLastName() . ' ' . $entity->getFirstName();
}
public function getValue(object $entity): string
{
return $entity->getId();
}
public function isGranted(Security $security): bool
{
// see the "security" option for details
return true;
}
}
Does anyone have an idea, answer or advice?
Does anyone have an idea, answer or advice?
I think your question is rather unrelated to the topic. The compile error is telling you what's the problem, you'retrying to re-use an existing alias to the User entity. Here are my thoughts, without knowing the rest of your schema:
->createQueryBuilder('ContactsApplozMember') ->select('(ContactsMember.contactUser) as id, (User.lastName) as lastName, (User.firstName) as firstName, (User.email) as email, (User.alias) as alias')
You're creating the query builder with alias ContactsApplozMember but then are using ContactsMember - is the snippet above meant to work?
->leftJoin('App\Entity\User', 'User', 'WITH', 'ContactsMember.contactUser = User.id')
Do you not have ContactsMember->User association? I assume ContactsMember is an inverse side, User is the owning side. You could join like this: ->leftJoin('ContactsMember.user', 'User').
I suggest getting rid of the join in the first round to see if the query starts working. Then add the join, see if it still works. Then add the additional selects. Play with different aliases. But most importantly, try to use the join as Doctrine wants you to, if you do have that association defined. Check this out https://symfonycasts.com/screencast/symfony3-doctrine-relations/query-with-join#adding-the-join
Also your use of parenthesis in (ContactsMember.contactUser) as id is unnecessary.
Good luck debugging!
@janklan Sorry for this confusion of problem... But your answers helped me a lot, I managed to debug!! Thank you very much 👍
Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?
Friendly ping? Should this still be open? I will close if I don't hear anything.
This continues to be a feature I'd love to have, to replace https://github.com/tetranz/select2entity-bundle
Something like this @tacman & @weaverryan, no?
->add('answer', AnswerAutoCompleteField::class, [
'autocomplete_url' => $this->router->generate('ux_entity_autocomplete', [
'alias' => 'answer_auto_complete_field',
'question' => $question->getId(),
]),
]),
#[AsEntityAutocompleteField]
class AnswerAutoCompleteField extends AbstractType
{
public function __construct(protected RequestStack $request) {}
public function configureOptions(OptionsResolver $resolver): void
{
$request = $this->request->getCurrentRequest();
$resolver->setDefaults([
'class' => Answer::class,
'query_builder' => function($answerRepo) use ($request) {
return $answerRepo->createQueryBuilder('a')
->andWhere('a.question := question')
->setParameter('question', $request->query->get('question'))
;
},
]);
}
public function getParent(): string
{
return BaseEntityAutocompleteType::class;
}
}
A better way to get the alias name answer_auto_complete_field? AsEntityAutocompleteField::shortName()?