Skip to content

[Form] added CallbackChoiceLoader and refactored ChoiceType's children #18332

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

Merged
merged 2 commits into from
Jun 13, 2016

Conversation

HeahDude
Copy link
Contributor

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets ~
License MIT
Doc PR todo

Todo

  • Address a doc PR
  • Update CHANGELOG

Changes

  • 39e937f added CallbackChoiceLoader to lazy load choices with a simple callable.
  • 995dc56 refactored CountryType, CurrencyType, LanguageType, LocaleType and TimezoneType for better performance by implementing ChoiceLoaderInterface for lazy loading.

Usage

use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

$builder->add('constants', ChoiceType::class, array(
    'choice_loader' => new CallbackChoiceLoader(function() {
            return StaticClass::getConstants();
    },
));

{
return 'currency';
if (null === self::$currencies) {
Copy link
Member

Choose a reason for hiding this comment

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

storing them in a static property will increase the memory usage by preventing garbage collection until the end of the process.
What kind of performance gain is there in term of time here, to counter-balance the loss regarding memory perf ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those types may be used many times in a form or with many forms in one request.

This optimisation is already done in TimeZoneType.

I have no hard feeling about this, just a PoC for now.

Copy link
Member

Choose a reason for hiding this comment

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

Sure they can be used many times. But any such optimization like this must be backed by numbers (especially when it is expected to improve one side at the expense of a loss on the other side, to know whether the loss can be accepted)

Copy link
Member

Choose a reason for hiding this comment

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

anyway, I would suggest splitting this caching outside this PR, to discuss it separately (this caching can even be applied without the refactoring done in this PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. Thanks @stof for the review!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stof this PR is now about the caching in a first commit and a refactoring in a second commit.

For choice_filter and a better approach in the implementation see #18334.

I would very much appreciate some feedback there before adding more tests. Thanks.

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch 5 times, most recently from 4caba21 to dff2883 Compare March 28, 2016 04:33
@HeahDude HeahDude changed the title [Form] add ImmutableChoiceType and choice_filter option to ChoiceType [Form] optimize ChoiceType children cache and add ImmutableChoiceType Mar 28, 2016
@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch from dff2883 to d3f6767 Compare March 28, 2016 04:45
@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch 3 times, most recently from 6360f60 to fbda538 Compare March 29, 2016 07:05
@HeahDude HeahDude changed the title [Form] optimize ChoiceType children cache and add ImmutableChoiceType [Form] add ImmutableChoiceType Mar 29, 2016
@HeahDude
Copy link
Contributor Author

Ok so I dropped 8298b1d because the CachingFactoryDecorator (ref #16747) already do the job, then there is no gain in caching the array_flip even if it is justified for TimezoneType.

I still think this abstract class is good for DX and maintainability. For example introducing the choice_translation_domain option would have required one line to this abstract class instead of modifying 5 classes.
And extending ImmutableChoiceType to only implement loadChoices is better for DX than implementing ChoiceLoaderInterface.

This may be trivial but there is no harm in performance, I've tried this code in SE:

$builder = $this->createFormBuilder();

for ($i = 0; $i < 10; ++$i) {
    $builder->add('country'.$i, FormType\CountryType::class);
    $builder->add('currency'.$i, FormType\CurrencyType::class);
    $builder->add('language'.$i, FormType\LanguageType::class);
    $builder->add('locale'.$i, FormType\LocaleType::class);
    $builder->add('timezone'.$i, FormType\TimezoneType::class);
}
$form = $builder->getForm();

From base to 8298b1d (try to improve cache with static properties:
10_form_base_to_cache

And from base to refactoring (current PR):
10_from_base_to_refactor

@HeahDude
Copy link
Contributor Author

ping @webmozart, does it fit with #18368 ?

@webmozart
Copy link
Contributor

A simpler and more flexible solution would be to add a CallableChoiceLoader. To the constructor of that loader, we can pass a callable in each of these types that loads the choices, e.g.:

class CountryType extends AbstractType
{
    private $choiceLoader;

    public function __construct()
    {
        $this->choiceLoader = new CallableChoiceLoader(function () {
            return array_flip(Intl::getRegionBundle()->getCountryNames());
        });
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'choice_loader' => $this->choiceLoader,
        ]);
    }
}

For each type, we make sure to reuse the same loader instance, then the caching mechanisms in ChoiceType will automatically take effect.

@HeahDude
Copy link
Contributor Author

I thought about it too since it is very easy to do, I'm glad you brought that up, I might be done in time :)

Should I go on with it ?

@stof
Copy link
Member

stof commented Mar 30, 2016

I prefer the solution suggested by @webmozart

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch from fbda538 to 3f8525f Compare March 31, 2016 04:49
@HeahDude
Copy link
Contributor Author

Rebased.

I first thought of using choice_loader as any callable when dealing with #18334, but I was not sure that it would be good to add an interface for that, although that's looks very great for those sub types!

I think this callable choice loader will really improve performances, I will try to profile something today :)

Also, I still think of some things:

  • it would be good to decouple the need to inherit from choice type basis from the need to load choices in a permanent way to ease maintaining them.

    This was originally sent as PoC, but it would be good to find a better name for such an abstract class:
    LoadedChoiceType or AbstractLazyChoiceType, it would be a more generic core version of DoctrineType.

  • I feel TimezoneType is looking really good now but the four others could be more optimised soon (in regard of Simplify access to CLDR/ICU data #18368) to directly implement the ChoiceLoaderInterface as we can do for mappers or transformers (e.g [Form] Let TextType implement DataTransformerInterface #18357).

Concerning choice loaders in general, if this PR is merged, this would be the first one implemented in the form core and in regard of the work I'm trying to do in #18359, I think there is two much caching when using loaders here's a schema:

Needs choice list (configuration | loading form types)
|
=> factory
   |
   |__ from choices ?
   |   |
   |   |__ decorated with cache ?
   |   |   |      
   |   |   |__ => return ArrayChoiceList
   |   |
   |   |
   |   |__ decorates
   |   |
   |   |__ => creates
   |       |
   |       |__ => cache => return ArrayChoiceList 
   |
   |
   |__ from loader ?
       |
       |__ decorated with cache ?
           |  
           |__ => return ArrayChoiceList
           |
           |
           |__ decorates
               |
               |__ => cache (*1) and return **LazyChoiceList**


Then later or not uses choice list (actually build form or view)
|
=> not lazy ?
    |
    |__ => return classic (good)
    |
    |__ is loaded ?
        |
        |__ => return classic
        |
        |__ call loader
            |
            |__ is loaded ?
            |   |
            |   |__ => return classic
            |   |
            |   |__ call factory
            |   |   |
            |   |   |__ decorated with cache
            |   |   |    |
            |   |   |    |__ wtf ?
            |   |   |
            |   |   |__ decorated with property path ?
            |   |   |    |
            |   |   |    |__ wtf ?
            |   |   |
            |   |   |__ factory creates, cache (*2) and return ArrayChoiceList
            |   |
            |   |__ loader cache (*3)
            |
            |__ lazy choice list cache (*4)

The (*n) marker indicates when the caching implies the number of copy of the same choice list.

So I suggest to use the following code in LazyChoiceList:

    public function getChoices()
    {
        // BC
        if (!$this->isLoaded) {
            $this->loadedList = $this->loader->loadChoiceList($this->value);
            $this->isLoaded = true;
        } else {
            if ($this->loadedList !== $loadedList = $this->loader->loadChoiceList($this->value) {
                // Trigger deprecation notice for caching in lazy choice list
                // not caching in choice loader unsupported in 4.0
                return $this->loadedList->getChoices();
            }
        }
        // End bc
        return $this->loader->loadChoiceList($this->value)->getChoices();
    }

And together with the change of #18359 we can get in 4.0:

Uses choice list (actually build form or view)
|
=> not lazy ?
    |
    |__ => return classic (good)
    |
    |
    |__ call loader
        |
        |__ is loaded ?
            |
            |__ => return classic
            |
            |__ loader creates, cache (*2) and returns ChoiceListInterface

I think the loader should not know about factories. The DoctrineChoiceLoader is not registered as service, it is only instantiated by default in the DoctrineType which passes it the factory so it's not really an extension point.

What do you think ?

Thanks.

public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
if (empty($value)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

$values? Is this not tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typo...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The choice loader should not be responsible for $value check or test IMHO.

@HeahDude
Copy link
Contributor Author

When exactly will 3.1 be feature frozen ?

* @var ArrayChoiceList
*/
private $choiceList;

Copy link
Member

Choose a reason for hiding this comment

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

please add documentation about what the callable is expected to return (namely an array of choices)

Copy link
Member

Choose a reason for hiding this comment

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

@HeahDude You seemed to agree, but I don't see any documentation here. Can you add a phpdoc? I would then remove the doc on the property above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@fabpot Sorry, I don't understand where you want me to add and remove things.

Back then I had changed the first phpdoc from The callable used to load the choices. to The callable used to load the array of choices. thinking that both phpdocs would be less confusing.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I was not very clear. Let me try again. It would be better to add a phpdoc to the constructor:

 /**
  * @param callable $callback The callable used to load the array of choices
  */
public function __construct(callable $callback)
 {
     $this->callback = $callback;
}

and remove the one on the private property:

private $callback;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, makes sense. I will do it very soon.

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch 2 times, most recently from 8e8bf19 to 64d1d9c Compare April 7, 2016 15:59
@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 7, 2016

@stof All comments addressed here, plus the optimization by implementing the ChoiceLoaderInterface directly!

This one still has a chance. I'm just waiting for the appveyor tests to be green :)

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch from 64d1d9c to d8539aa Compare April 7, 2016 16:02
@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 7, 2016

Green :)

@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 7, 2016

I will try to profile again but there's no doubt that they'll be better than above :)

/**
* @author Jules Pietri <jules@heahprod.com>
*/
abstract class AbstractChoiceLoaderTest extends \PHPUnit_Framework_TestCase
Copy link
Member

Choose a reason for hiding this comment

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

IMO, there is no need to introduce this abstract class, as you use it only once

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't you think it might help to introduce other loaders in the future and test them ?
I did it because it's the first implementation in the form component (the only one before was the DoctrineChoiceLoader) but this part should be re-usable IMHO.

Of courses I can easily integrate it in CallbackChoiceLoaderTest class.

Is this the final blocker ? Thank you again for your review.

Copy link
Member

Choose a reason for hiding this comment

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

@HeahDude We can still extract common logic in an abstract base class then if needed. I would remove this class for now too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough! I do it right away.

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch from d8539aa to 8e3b279 Compare April 11, 2016 10:11
@HeahDude
Copy link
Contributor Author

Alright, last comments addressed! Failures are unrelated, I think I'm done here :)

@fabpot
Copy link
Member

fabpot commented Apr 11, 2016

👍

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch from 8e3b279 to ec0fa1f Compare April 25, 2016 13:45
@HeahDude
Copy link
Contributor Author

Rebased. Tests still green.

Last comment here is @fabpot approval. Is this still planned for 3.1? If so, it should be merged as soon as possible to be tested during beta.

ping @symfony/deciders

Thanks.

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch 2 times, most recently from 8607c56 to 72173aa Compare April 30, 2016 06:15
@HeahDude
Copy link
Contributor Author

Re-re-rebased :)

@HeahDude HeahDude force-pushed the feature-form-immutable_choice_type branch from 72173aa to 8a4e164 Compare June 13, 2016 10:05
@HeahDude
Copy link
Contributor Author

Comments addressed and changelog updated to target 3.2.

@fabpot
Copy link
Member

fabpot commented Jun 13, 2016

@HeahDude Can you submit a PR for the docs? Thanks.

@fabpot
Copy link
Member

fabpot commented Jun 13, 2016

Thank you @HeahDude.

@fabpot fabpot merged commit 8a4e164 into symfony:master Jun 13, 2016
fabpot added a commit that referenced this pull request Jun 13, 2016
…iceType's children (HeahDude)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Form] added `CallbackChoiceLoader` and refactored ChoiceType's children

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | ~
| License       | MIT
| Doc PR        | todo

Todo
====
- [ ] Address a doc PR
- [x] Update CHANGELOG

Changes
=======
 - 39e937f added `CallbackChoiceLoader` to lazy load choices with a simple callable.

 - 995dc56 refactored `CountryType`, `CurrencyType`, `LanguageType`, `LocaleType` and `TimezoneType` for better performance by implementing `ChoiceLoaderInterface` for lazy loading.

Usage
=====
```php
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

$builder->add('constants', ChoiceType::class, array(
    'choice_loader' => new CallbackChoiceLoader(function() {
            return StaticClass::getConstants();
    },
));
```

Commits
-------

8a4e164 [Form] implemented ChoiceLoaderInterface in children of ChoiceType
afd7bf8 [Form] added `CallbackChoiceLoader`
@HeahDude
Copy link
Contributor Author

Will do, thank you @fabpot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants