Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Auto-complete Association Field & Controlling the Query

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The AssociationField creates these pretty cool select elements. But these are really just normal, boring select elements with a fancy UI. All of the options, in this case, every user in the database, is loaded onto the page in the background to build the select. This means that if you have even a hundred users in your database, this page is going to start slowing down, and eventually, explode.

AssociationField::autoComplete()

To fix this, head over and call a custom method on the AssociationField called ->autocomplete():

... lines 1 - 11
class QuestionCrudController extends AbstractCrudController
{
... lines 14 - 18
public function configureFields(string $pageName): iterable
{
... lines 21 - 28
yield AssociationField::new('askedBy')
->autocomplete();
... lines 31 - 32
}
}

Yes, this is as nice as it sounds. Refresh. It looks the same, but when we type in the search bar... and open the Network Tools... check it out! That made an AJAX request! So instead of loading all of the options onto the page, it leverages an AJAX endpoint that handles the autocompletion. Problem solved!

Controlling the Autocomplete Items with formatValue()

And as you can see, it uses the __toString() method on User to display the option, which is the same thing it does on the index page in the "Asked By" column. We can control that, however. How? We already know: it's our old friend ->formatValue(). As you might remember, this takes a callback function as its argument: static function() with $value as the first argument and Question as the second:

... lines 1 - 11
class QuestionCrudController extends AbstractCrudController
{
... lines 14 - 18
public function configureFields(string $pageName): iterable
{
... lines 21 - 28
yield AssociationField::new('askedBy')
... line 30
->formatValue(static function ($value, Question $question): ?string {
... lines 32 - 36
});
... lines 38 - 39
}
}

The $value argument will be the formatted value that it's about to print onto the page. And then Question is the current Question object. We'll eventually need to make this argument nullable and I'll explain why later. But for now, just pretend that we always have a Question to work with.

Inside: if (!$question->getAskedBy()) - if for some reason that field is null, we'll return null. If that is set, return sprintf() - with %s,   for a space, and then %s inside of parentheses. For the first wildcard, pass $user->getEmail().

Oh, whoops! In the if statement, I meant to say if !$user =. This, fancily, assigns the $user variable and checks to see if there is an askedBy user all at once. Finish the ->getEmail() method and use $user->getQuestions()->count() for the second wildcard:

... lines 1 - 11
class QuestionCrudController extends AbstractCrudController
{
... lines 14 - 18
public function configureFields(string $pageName): iterable
{
... lines 21 - 28
yield AssociationField::new('askedBy')
... line 30
->formatValue(static function ($value, Question $question): ?string {
if (!$user = $question->getAskedBy()) {
return null;
}
return sprintf('%s (%s)', $user->getEmail(), $user->getQuestions()->count());
});
... lines 38 - 39
}
}

HTML IS Allowed in EasyAdmin

Oh, and about that  . I added this, in part, to show off the fact that when you render things in EasyAdmin, you can include HTML in most situations. That's normally not how Symfony and Twig work, but since we're never configuring EasyAdmin based off of user input... and this is all just for an admin interface anyways, EasyAdmin allows embedded HTML in most places.

Ok, let's check things out! Reload and... boom! We get our new "Asked By" format on the index page.

The real reason I wanted us to do this was to point out that the formatted value is used on the index page and the detail page... but it is not used on the form. The form always uses the __toString() method from your entity.

Controlling the Autocomplete Query

One of the things we can control for these association fields is the query that's used for the results. Right now, our autocomplete field returns any user in the database. Let's restrict this to only enabled users.

How? Once again, we can call a custom method on AssociationField called ->setQueryBuilder(). Pass this a function() with a QueryBuilder $queryBuilder argument:

... lines 1 - 5
use Doctrine\ORM\QueryBuilder;
... lines 7 - 12
class QuestionCrudController extends AbstractCrudController
{
... lines 15 - 19
public function configureFields(string $pageName): iterable
{
... lines 22 - 29
yield AssociationField::new('askedBy')
... lines 31 - 38
->setQueryBuilder(function (QueryBuilder $qb) {
... lines 40 - 41
});
... lines 43 - 44
}
}

When EasyAdmin generates the list of results, it creates the query builder for us, and then we can modify it. Say $queryBuilder->andWhere(). The only secret is that you need to know that the entity alias in the query is always entity. So: entity.enabled = :enabled, and then ->setParameter('enabled', true):

... lines 1 - 12
class QuestionCrudController extends AbstractCrudController
{
... lines 15 - 19
public function configureFields(string $pageName): iterable
{
... lines 22 - 29
yield AssociationField::new('askedBy')
... lines 31 - 38
->setQueryBuilder(function (QueryBuilder $qb) {
$qb->andWhere('entity.enabled = :enabled')
->setParameter('enabled', true);
});
... lines 43 - 44
}
}

That's it! We don't need to return anything because we modified the QueryBuilder. So let's go see if it worked!

Well, I don't think we'll notice any difference because I'm pretty sure every user is enabled. But watch this. When I type... here's the AJAX request. Open up the web debug toolbar... hover over the AJAX section and click to open the profiler.

You're now looking at the profiler for the autocomplete AJAX call. Head over to Doctrine section so we can see what that query looks like. Here it is. Click "View formatted query". Cool! It looks on every field to see if it matches the %ti% value and it has WHERE enabled = ? with a value of 1... which comes from up here. Super cool!

Next: could we use an AssociationField to handle a collection relationship? Like to edit the collection of answers related to a Question? Totally! But we'll need a few Doctrine & form tricks to help us.

Leave a comment!

18
Login or Register to join the conversation
Dmitry-K Avatar
Dmitry-K Avatar Dmitry-K | posted 26 days ago

Hi!
Is where any way to set custom url for autocomplete request for Association Field?

Reply

Hey Dmitry,

Well, that's not configurable, unfortunately, but you can override that autocomplete method in the CRUD controller to which it sends AJAX requests and do there whatever you want.

I hope this helps!

Cheers!

Reply
Klaus-M Avatar
Klaus-M Avatar Klaus-M | posted 28 days ago

I've a many to many relation solved by a third entity because i needed some extra fields.

Now I want to display these extra fields in the edit section of the CRUD Controller wich the entity is related on.

Some facts, the entity is called runSheets and the many to many relation entity is called runSheetStations that combines runSheets and stations. The runSheetsStations has two extra fields done (boolean) and memo (text).

How can I create fields that will represent those fields and how could I save changes.

Thank you very much für your help.

Reply

Hey @Klaus-M!

Hmm. Ok, so if I understand correctly, when editing a single runSheets entity, you would like to show a "collection" of sub-forms for each related runSheetStations where each sub-form shows the done and memo field. Is that correct?

If so, the solution should be to use the CollectionField - https://symfony.com/doc/4.x/EasyAdminBundle/fields/CollectionField.html - for the runSheetStations property on runSheets (I'm assuming that your runSheets entity has a runSheetsStations property which is a OneToMany to the runSheetStations entity). That... should be all you need to do (though, this stuff is complex enough that I would NOT be surprised if I'm forgetting a detail). The CollectionField stuff can get really complex... but that's mostly if you need to be able to dynamically add new relations... but it doesn't sound like you even have that case: you just want the user to be able to modify some fields on the existing, related runSheetStations.

Let me know if that helps :). Cheers!

Reply
JoshuaGugun Avatar
JoshuaGugun Avatar JoshuaGugun | posted 1 month ago

Hello,

What does the difference of `static function` and `function` of anonymous-function used on this chapter?

Reply

Hey suabahasa,

Good question! Though it's not that much important fairly speaking, but I'll try to explain you some details. Just functions are bind to the instance context, i.e. you can use $this inside those functions, i.e. they are bind to the *objects* of classes where you use them. When static functions are bind to the class, i.e. they do not have access to the $this variable inside of them. And so, they are mostly like static and regular methods on its behaviour, and also has similar performance. If you're not going to use $this inside those functions - it's recommend to make them static that will have a better performance, but that is minor and difficult to be noticed anyway, but depends on your project code complexity. So, it's mostly like a "hipster" question, use static when you could to be more hipster :p ;)

I hope it's clearer for you now.

Cheers!

Reply
JoshuaGugun Avatar
JoshuaGugun Avatar JoshuaGugun | victor | posted 1 month ago

Hi Victor,

thank you for detailed answer.
Before, I curious if there is some special use case/best-practice of using the "static" for this chapter, but now I got the answer.

Cheers!

Reply

Hey Suabahasa,

Awesome, glad to help! :)

Cheers!

Reply
Peter A. Avatar
Peter A. Avatar Peter A. | posted 6 months ago

I have an employee entity that has a self-refencing field supervisor (which is a ManyToOne Association).
In EmployeeCrudController, I have in ConfigureFields -- yield AssociationField::new('supervisor'). This works fine when I am creating new Employee.
Employee:Peter <== Supervisor:John
But when I want to edit the above for employee:Peter, the Association Field Supervisor will include the employee Peter itself as one of the choices. I thought of using the querybuilder but I can't think of how to get the current record to be edited so I can filter it out.

Reply

Hey Peter,

Ah, I see your problem. Yeah, that's a tricky case. You're on the correct way, you would need to use a custom query builder to filter that out. How to get the current Employee ID? Take it from the URL. If you look closer at the URL - you will notice that the current employee ID is written there as "entityId". So, inject RequestStack into your controller, get the current Request object from it and get the "entityId" from the query. Then, just use it in the query builder to filter it out.

Or, probably even better, you can leverage EasyAdmin getContext() to get the current Request object, i.e. call "$this->getContext()->getRequest()". Btw, you can even try to get the current entity object as "$this->getContext()->getEntity()->getInstance()", but it might be overkill in case you just need its ID - you just can fetch it from the URL query parameter.

I hope this helps!

Cheers!

Reply
Peter A. Avatar

nice..Thanks

Reply
Tomáš S. Avatar
Tomáš S. Avatar Tomáš S. | posted 6 months ago

Hello,

It is possible to use methods from Entity's Repository (ie findBy...()) instead of specifying it on QueryBuilder?

Thanks for reply

Reply

Hey Tomáš Skočdopole!

Excellent question! It is *not*, unfortunately. The technical reason is that, in addition to querying for all the items to list them, iirc, the QueryBuilder is also used when an item is *submitted* to figure out if the submitted item is a "valid" item. For example, it would take your QueryBuilder (which may already have some WHERE on it) and (again, I'm going off memory here, but I believe this is how it works) add a AND id = 12 where 12 is the id of the submitted item. If the system allowed you to only pass it the findBy(), then even for just checking the validity of the submitted item, it would need to use your method to query for ALL of the items... just to check if the submitted one is in there.

Anyways, the "best answer" I can give you is this: if you already have an existing, custom findByABC() method in your repository, and you'd like to re-use that logic, then isolate your custom QueryBuilder logic into some createFindByABCQueryBuilder(QueryBuilder $qb = null): QueryBuilder method. Then, call this from your custom findByABC() method and when working with this field.

I hope that helps :).

Cheers!

Reply
Tomáš S. Avatar

Thank you.

I am absolutely beginner of Symfony and EasyAdminBundle. It would be nice to reuse the code, but it is not important at the moment for me.

Best regards

Reply
Debasish N. Avatar
Debasish N. Avatar Debasish N. | posted 6 months ago

How can i perform dependent select

Reply

Hey Debasish Nandi

Could you elaborate a bit more? I don't quite understand what you mean by "dependent select"

Cheers!

Reply
Debasish N. Avatar

Thanks, @Diego Aguiar
I have Category and Subcategory entities. Now on the product creation
page, I need to select category and subcategory. When I select a
category then I want to show the subcategories which belong to the
selected category. How can I do that in easy admin?

Reply

Hey Debasish,

Unfortunately, that's not available our of the box. I suppose you can create a custom EasyAdmin filter in this case, and with some AJAX requests it's doable. I'm not sure if you can reuse the exist EasyAdmin filter for the root categories, it still might be tricky, so you may need to implement your custom filter for both root categories and subcategories. Unfortunately, we do. not cover such a complex case in this tutorial, but I think it's possible to do in theory with some custom JS code.

I hope this helps!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0
    }
}