Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DoctrineBridge][Form] Introducing new LazyChoiceLoader class and choice_lazy option for ChoiceType #52503

Merged
merged 1 commit into from
Oct 9, 2024

Conversation

yceruto
Copy link
Member

@yceruto yceruto commented Nov 8, 2023

Q A
Branch? 7.2
Bug fix? no
New feature? yes
Deprecations? no
Issues #57724
License MIT

It's quite usual to work with forms that process large datasets. In Symfony Form + Doctrine ORM, if you define an EntityType, it typically loads all choices/entities fully into memory, and this can lead to serious performance problems if your entity table contain several hundred or thousands of records.

The new LazyChoiceLoader class addresses this performance issue by implementing an on-demand choice loading strategy. This class is integrated with any ChoiceType subtype by using a new boolean option named choice_lazy, which activates the feature.

Basic usage in a Symfony form looks like this:

$formBuilder->add('user', EntityType::class, [
    'class' => User::class, // a ton of users...
    'choice_lazy' => true,
]);

How does it work?

The loader operates by keeping the choice list empty until values are needed (avoiding unnecessary database queries). When form values are provided or submitted, it retrieves and caches only the necessary choices.

As you can see in the code, all this happens behind the LazyChoiceLoader class, which delegates the loading of choices to a wrapped ChoiceLoaderInterface adapter (in this case, the DoctrineChoiceLoader).

Frontend Considerations

Certainly, you may need a JavaScript component for dynamically loading <select> options, aka autocomplete plugins. You'll need to develop the endpoint/controller to fetch this data on your own, ensuring it corresponds to the form field data source. This aspect is not included in this project.

As a point of reference, the Autocomplete UX Component now uses this choice loading strategy, simplifying its autocomplete form type to a single field:

A Handy Use Case without Javascript?

The disabled option renders an EntityType form field read-only, and when combined with the choice_lazy option, it prevents the loading of unnecessary entities in your choice list (only the pre-selected entities will be loaded), thereby enhancing performance.


Hope this helps to create simpler autocomplete components for Symfony forms.

Cheers!

@yceruto yceruto added this to the 7.1 milestone Nov 8, 2023
@yceruto yceruto requested a review from weaverryan November 8, 2023 21:24
@yceruto yceruto requested a review from xabbuh as a code owner November 8, 2023 21:24
@carsonbot carsonbot modified the milestones: 7.1, 7.0 Nov 8, 2023
@carsonbot

This comment was marked as outdated.

@yceruto yceruto modified the milestones: 7.0, 7.1 Nov 8, 2023
@yceruto yceruto changed the title [Form] Introducing new ExtraLazyChoiceLoader class and extra_lazy option for DoctrineType [Form][DoctrineBridge] Introducing new ExtraLazyChoiceLoader class and extra_lazy option for DoctrineType Nov 8, 2023
@OskarStark OskarStark changed the title [Form][DoctrineBridge] Introducing new ExtraLazyChoiceLoader class and extra_lazy option for DoctrineType [Form][DoctrineBridge] Introducing new ExtraLazyChoiceLoader class and extra_lazy option for DoctrineType Nov 8, 2023
@carsonbot carsonbot changed the title [Form][DoctrineBridge] Introducing new ExtraLazyChoiceLoader class and extra_lazy option for DoctrineType [DoctrineBridge][Form] Introducing new ExtraLazyChoiceLoader class and extra_lazy option for DoctrineType Nov 8, 2023
@yceruto
Copy link
Member Author

yceruto commented Nov 8, 2023

Over there, there should be other special use cases, such as using API payloads against forms where the choice lists are not really necessary, so people work around with custom form types and special EntityToIdDataTransformer. Now they can use the EntityType directly with the choice_lazy option activated.

@Seb33300
Copy link
Contributor

Seb33300 commented Nov 9, 2023

Over there, there should be other special use cases, such as using API payloads against forms where the choice lists are not really necessary, so people work around with custom form types and special EntityToIdDataTransformer. Now they can use the EntityType directly with the extra_lazy option activated.

+1

I had a similar case few years ago with an API relying on a Symfony form with an EntityType loading thousands of choices.
I ended by adding the EntityType field to the form with an event on submit and filtering the SQL query to select only the items submitted by the user.

@Pixelshaped
Copy link

Pixelshaped commented Nov 9, 2023

Thank you I was already using this code from UX Autocomplete and I think it's the right move to make it part of Doctrine Bridge as this is a very common use case, even if part of the solution is front end (one would still need to implement a route to get a paginated list of entities and pass it to Select2 or whatever solution you're using).

I had duplicated the ExtraLazyChoiceLoader in my project to not require UX Autocomplete (as I'm using Select2 already everywhere), and now I can use this and stay updated.

Nice job.

@stof
Copy link
Member

stof commented Nov 9, 2023

I'm not sure this extra_lazy option in the existing type extending ChoiceType is the right option. Such autocomplete type requires a totally different rendering than a choice type (and this autocomplete rendering might make sense with data sources that are not from doctrine).

@yceruto
Copy link
Member Author

yceruto commented Nov 9, 2023

@stof certainly there's additional work required to create a fully-autocomplete type, which is not the goal of this PR btw. However, it does streamline implementations that previously needed custom/complex solutions. The choice_lazy option facilitates this for DoctrineType although its use is not restricted to that purpose alone.

I believe the LazyChoiceLoader is the main feature here. It's compatible with any ChoiceType and you can wrap any data source (Local Storage, External APIs, ...) allowing it to load those data efficiently by following this "on-demand" approach.

@yceruto yceruto changed the base branch from 7.0 to 7.1 November 15, 2023 18:04
@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from 4d42789 to 87afdea Compare November 15, 2023 18:04
@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from 87afdea to 53463a2 Compare December 4, 2023 21:42
@norkunas

This comment was marked as off-topic.

@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from 53463a2 to e933246 Compare January 6, 2024 11:47
@yceruto yceruto modified the milestones: 7.1, 7.2 May 2, 2024
@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from e933246 to 33b2f3d Compare July 3, 2024 19:25
@yceruto yceruto force-pushed the feature/lazy_choice_loader branch 3 times, most recently from 4016846 to fae0d67 Compare July 3, 2024 22:59
@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from fae0d67 to 2953bcf Compare July 17, 2024 13:35
@yceruto
Copy link
Member Author

yceruto commented Jul 17, 2024

Thanks @xabbuh for your review! I also added two test cases more about multiple option combined with choice_lazy.

@carsonbot carsonbot changed the title [Form] Introducing new LazyChoiceLoader class and choice_lazy option for ChoiceType [DoctrineBridge][Form] Introducing new LazyChoiceLoader class and choice_lazy option for ChoiceType Jul 19, 2024
@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from 2953bcf to 97ddd62 Compare August 4, 2024 01:13
@yceruto
Copy link
Member Author

yceruto commented Aug 4, 2024

Just rebased to fix conflicts.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe stupid comment: can't we always behave lazily when choice_loader is set and get rid of the option altogether?

@stof
Copy link
Member

stof commented Aug 22, 2024

@nicolas-grekas this lazy mode assumes that you have a form rendering that does not need the list of available choices (and so not rendering it as a <select> like the default rendering of a ChoiceType).

This is the reason why I suggested that it may fit better as a separate form type instead (which would have a separate rendering).

Making all choice loaders wrapped in this LazyChoiceLoader in ChoiceType itself would break it.

@yceruto yceruto force-pushed the feature/lazy_choice_loader branch from 97ddd62 to d73b5ee Compare August 23, 2024 01:42
@yceruto
Copy link
Member Author

yceruto commented Aug 23, 2024

@nicolas-grekas this lazy mode assumes that you have a form rendering that does not need the list of available choices (and so not rendering it as a <select> like the default rendering of a ChoiceType).

I can add that if there is data bound to that field (either default or submitted), it will render the choices as usual, rendering only the selected ones in this case.

This is the reason why I suggested that it may fit better as a separate form type instead (which would have a separate rendering).

I might be missing your point here. Still wondering how it could be different from the current one and why... you can currently have different renderings for the same form type using block_prefix option.

Making all choice loaders wrapped in this LazyChoiceLoader in ChoiceType itself would break it.

Sorry, I'm confused by this comment. Is it related to Nicolas' comment? Otherwise, could you please elaborate 🙏

@yceruto
Copy link
Member Author

yceruto commented Aug 23, 2024

Maybe stupid comment: can't we always behave lazily when choice_loader is set and get rid of the option altogether?

You might still want to use a choice_loader even without lazy loading.

@yceruto yceruto added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Oct 3, 2024
@yceruto
Copy link
Member Author

yceruto commented Oct 3, 2024

This is ready on my side. @stof any blocker on your side?

@nicolas-grekas
Copy link
Member

Thank you @yceruto.

@nicolas-grekas nicolas-grekas merged commit 5763273 into symfony:7.2 Oct 9, 2024
9 of 10 checks passed
@yceruto yceruto deleted the feature/lazy_choice_loader branch October 9, 2024 16:14
javiereguiluz added a commit to javiereguiluz/symfony-docs that referenced this pull request Oct 14, 2024
This PR was merged into the 7.2 branch.

Discussion
----------

[Form] Documenting ``choice_lazy`` option

Closes symfony#20311
Code PR
* symfony/symfony#52503

Commits
-------

e55e7cd documenting choice_lazy option
@fabpot fabpot mentioned this pull request Oct 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DoctrineBridge Feature Form ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants