-
-
Notifications
You must be signed in to change notification settings - Fork 323
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
[Autocomplete] Allow passing extra options to the autocomplete fields #1322
[Autocomplete] Allow passing extra options to the autocomplete fields #1322
Conversation
src/Autocomplete/src/Controller/EntityAutocompleteController.php
Outdated
Show resolved
Hide resolved
src/Autocomplete/src/Controller/EntityAutocompleteController.php
Outdated
Show resolved
Hide resolved
As I'm currently facing a similar issue in recreating some more complex Selection Type which is doing similar things like autocomplete. I wanted to share it here also here. https://gist.github.com/alexander-schranz/b1cb8843bc54dc368001375fe6998e61#file-mycontactchoicetype-php-L39 In my case I did also go with providing a list of options, so the loading part will have possibility todo such things. But instead of duplicating the options I just provide a list of which options should be forwarded (string[], instead of array<string, mixed>`. So I don't have to duplicate the value of the option inside the extra_options, item_loader_options in my case. |
I try currently some magic to find out automatically my public function buildView(FormView $view, FormInterface $form, array $options): void
{
$extraOptions = [];
$formType = $form->getConfig()->getType();
$innerType = $formType->getInnerType();
$defaultOptionsResolver = new OptionsResolver();
$innerType->configureOptions($defaultOptionsResolver);
$definedOptions = $defaultOptionsResolver->getDefinedOptions();
foreach ($defaultOptionsResolver->getMissingOptions() as $missingOption) {
$defaultOptionsResolver->remove($missingOption);
}
// Form, ChoiceType options which we keep always in mind and are important for form widget rendering e.g. required, disabled, ...
foreach ($options['extra_options'] as $extra_option) {
$definedOptions[] = $extra_option;
}
$defaultOptions = $defaultOptionsResolver->resolve([]);
foreach ($options as $key => $value) {
if (!\is_scalar($value)) {
// ignore all options which are non-scalar values
continue;
}
if (!\in_array($key, $definedOptions)) {
// we ignore all options which were not defined specifically for the inner type
continue;
}
if (isset($defaultOptions[$key]) && $defaultOptions[$key] === $value) {
// we ignore all options which have the same value as the default value
continue;
}
$extraOptions[$key] = $value;
}
$view->vars['extra_options'] = $extraOptions;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'extra_options' => ['required', 'disabled'],
]);
} What could also be used to avoid a seperate extra_options is the public function buildView(FormView $view, FormInterface $form, array $options): void
{
$extraOptions = [];
$formType = $form->getConfig()->getType();
$innerType = $formType->getInnerType();
$defaultOptionsResolver = new OptionsResolver();
$requiredOptions = $defaultOptionsResolver->getRequiredOptions();
foreach ($defaultOptionsResolver->getMissingOptions() as $missingOption) {
$defaultOptionsResolver->remove($missingOption);
}
$defaultOptions = $defaultOptionsResolver->resolve([]);
foreach ($options as $key => $value) {
if (!\is_scalar($value)) {
// ignore all options which are non-scalar values
continue;
}
if (!\in_array($key, $requiredOptions)) {
// we ignore all options which are not required
continue;
}
if (isset($defaultOptions[$key]) && $defaultOptions[$key] === $value) {
// we ignore all options which have the same value as the default value
continue;
}
$extraOptions[$key] = $value;
}
$view->vars['extra_options'] = $extraOptions;
} |
|
||
namespace Symfony\UX\Autocomplete\Calculator; | ||
|
||
final class ChecksumCalculator implements ChecksumCalculatorInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my own selection type I did instead of using a checksum go with the UriSigner
but ignore all query parameters which I don't know, so maybe we could do here the avoid own checksum calculator:
if (!$this->uriSigner->check(
// ignore unrelated query parameters as they can be search filters which are allowed
$request->getSchemeAndHttpHost() . $request->getPathInfo() . '?_config=' . \rawurlencode($request->query->get('_config')) . '&_hash=' . \rawurlencode($request->query->get('_hash')),
)) {
throw new AccessDeniedHttpException('Invalid signature.');
}
/**
* @var array{
* options: array<string, mixed>,
* name: string,
* theme: string,
* }|false
*/
$config = \json_decode(\base64_decode($request->query->get('_config'), true), true, 512, \JSON_THROW_ON_ERROR);
3e13fc6
to
74a9577
Compare
Do you want some help/feedback/other on this ? |
Hi @smnandre, I'll let you know once ready to be reviewed :D. It should be ready today :D. |
Great! Good luck then :) |
I guess I haven't missed anything, and it's ready to be reviewed 👍🏻. AFAIR LiveComponent build failures are not related to my PR. |
{ | ||
$this->sortKeysRecursively($data); | ||
|
||
return base64_encode(hash_hmac('sha256', json_encode($data), $this->secret, true)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use some lighter hash algo?
return base64_encode(hash_hmac('sha256', json_encode($data), $this->secret, true)); | |
return base64_encode(hash_hmac('xxh128', json_encode($data), $this->secret, true)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would the base64 still be necessary ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question here would be how we treat that data, for me it's checksum which is not a password but should be kinda of cryptographic, but it doesn't need to be perfect as a password and it doesn't need to be as safe as it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right. I thought (idk why :D) that we encrypt data here, but the data is not encrypted at all.
So the answer here is simple, I used sha256
, because that's exactly the same way as we calculate the checksum in the LiveComponent. I'm open to changing it, I just copied the code from the LiveComponent and didn't bother to think whether sha256
is the correct one here :D.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I'm thinking about extracting checksum calculating inside the LiveComponent component, and using it in the autocomplete component. Autocomplete depends on the LiveComponent so it might make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would make perfect sense to me.
Feel free to do it in a second/later PR if you want to release this one first... in all maner this should be "internal" so there is no BC involved there :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was 100% sure LiveComponent
is required by the Autocomplete
, but it is not.
So, for now, I leave the Calculator
where it lives. I can mark the class as internal
but I'll leave the interface as it is, to allow people to create their own implementation of checksum calculator. If anyone finds they need a lighter version of hash algorithm, it'll make sense to create their own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but I'll leave the interface as it is,
I'd prefer to remove the interface. I can't see a legit reason to need to write your own. If there is a problem with our class, then we want them to tell us so we can fix it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weaverryan I've removed it!
src/Autocomplete/src/Controller/EntityAutocompleteController.php
Outdated
Show resolved
Hide resolved
Somewhere in the future documentation, should we add a small warning about the visibility of extra_options ? Something like "the given extra options can be retrieved and therefore should not contain sensible or personal information" |
/** | ||
* Interface for classes that will have an "autocomplete" endpoint exposed with a possibility to pass additional options. | ||
*/ | ||
interface OptionsAwareEntityAutocompleterInterface extends EntityAutocompleterInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather have separate Interfaces, what do you think ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do like creating "subtype" interfaces like this, but it's not a thing I'd fight for :D. I'm open for changing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong opinion on this.
i'm often more concerned about "what could happen later" than i probably should... i'm working on it but habits are hard to change :)
That's why i ask / raise questions but would never "require" changes :))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments, but this is really great! Could you also bootstrap docs for this? Thanks!
{ | ||
$this->sortKeysRecursively($data); | ||
|
||
return base64_encode(hash_hmac('sha256', json_encode($data), $this->secret, true)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but I'll leave the interface as it is,
I'd prefer to remove the interface. I can't see a legit reason to need to write your own. If there is a problem with our class, then we want them to tell us so we can fix it :)
private function getExtraOptions(Request $request): array | ||
{ | ||
if (!$request->query->has(self::EXTRA_OPTIONS)) { | ||
return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we used Symfony's UriSigner
instead of the checksum? If extra options exist, we sign the URL when generating it and passing it to the frontend. Here, we validate the signed URL.
src/Autocomplete/src/OptionsAwareEntityAutocompleterInterface.php
Outdated
Show resolved
Hide resolved
src/Autocomplete/src/OptionsAwareEntityAutocompleterInterface.php
Outdated
Show resolved
Hide resolved
Friendly ping :) - I'd love to get this across the finish line. Thanks! |
Hi @weaverryan, |
2b6b26c
to
db61002
Compare
1c21c2c
to
0ff4d2d
Compare
|
||
namespace Symfony\UX\Autocomplete\Checksum; | ||
|
||
/** @internal */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** @internal */ | |
/** | |
* @author .... ? | |
* | |
* @internal | |
*/ |
(same thing on OptionsAware..Interface ?)
@@ -11,6 +11,8 @@ | |||
- Added `tom-select/dist/css/tom-select.bootstrap4.css` to `autoimport` - this | |||
will cause this to appear in your `controllers.json` file by default, but disabled | |||
see. | |||
- Allow passing `extra_options` key in an array passed as a `3rd` argument of the `->add()` method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compared to symfony/form changelog, i'd write it maybe more like
- Add
extra_options
to XXX and YYY types (those options are .... to ...)
0ff4d2d
to
308daef
Compare
Thanks for your hard work on this Jacob! |
Not sure if this is bad etiquette, but if anyone came to this thread looking for how to use this new feature, an example of how to use it is in this thread, it's just hidden by GitHub. It's right here. |
Oh yea, it looks like we missed creating a docs PR. @jakubtobiasz are you able to create a quick docs PR for this feature? |
…e Ajax-powered autocomplete (jakubtobiasz) This PR was merged into the 2.x branch. Discussion ---------- [LiveComponent] Document how to use `extra_options` in the Ajax-powered autocomplete | Q | A | ------------- | --- | Bug fix? | no | New feature? | n/a | Issues | requested in #1322 (comment) | License | MIT > Time spent on this PR has been sponsored by [Commerce Weavers](https://commerceweavers.com/)♥️ . Commits ------- aab8892 [LiveComponent] Document how to use `extra_options` in the Ajax-powered autocomplete
…e Ajax-powered autocomplete (jakubtobiasz) This PR was merged into the 2.x branch. Discussion ---------- [LiveComponent] Document how to use `extra_options` in the Ajax-powered autocomplete | Q | A | ------------- | --- | Bug fix? | no | New feature? | n/a | Issues | requested in symfony/ux#1322 (comment) | License | MIT > Time spent on this PR has been sponsored by [Commerce Weavers](https://commerceweavers.com/)♥️ . Commits ------- aab88929 [LiveComponent] Document how to use `extra_options` in the Ajax-powered autocomplete
… attributes (pierredup) This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Autocomplete] Reset TomSelect when updating controller attributes | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Issues | Fix #1500 | License | MIT When using remote data for Autocomplete, the mutation observer is not started, so the select cannot be reset when some options change (E.G when limiting the options returned after selecting other options using extra_options from #1322). Instead, we start the mutation observer when using remote data. When the observer runs and checks if the options have changes, we explicitly set the `areOptionsEquivalent` to `true`, since the page will just render an empty `select`. We also check if any of the `data-autocomplete` attributes changes and forces a reset (E.G using extra_options, the `data-autocomplete-url` value can be different when using extra options, so we reset the select when this attribute changes). This fixes only one of the issues from #1500, I'll investigate the other issues and create separate PRs. Commits ------- d8fb3fb [Autocomplete] Reset TomSelect when updating controller attributes
This PR still needs some tweaks and tests, but I want to validate my idea. Currently, if we pass anything as a third argument in the
->add()
method, it is only used on the form render, and is fully ignored during the AJAX call.While implementing UX's Autocomplete in Sylius, I wanted to complete the following user story
So basically, I have to pass a current taxon's ID to the autocomplete's query. As we know, currently it's not possible. So after contacting @weaverryan, I decided to implement a mechanism similar to the one from Live Components.
When you pass an array of options as a
3rd
argument, you can use a specialextra_options
key, which is an array consistingscalars
/arrays
/nulls
.Next, when the form is rendered, I get these values, calculate a checksum for them, then pass them through
json_encode
andbase64_encode
functions. In the end, I glue them to theurl
values in the\Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension::finishView
method.So, basically with the following configuration:
we end up with the following HTML code:
I decided to "glue" the
extra_options
to the URL, as I didn't have to deal with JS. Of course, I do not exclude a chance to refactor it, as a whole method should be refactored anyway.Finally, the controller decodes the data, checks the checksum and passes the values to the autocomplete's form.