forked from symfony/ux
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature symfony#1322 [Autocomplete] Allow passing extra options to th…
…e autocomplete fields (jakubtobiasz) This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Autocomplete] Allow passing extra options to the autocomplete fields | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | n/a | License | MIT 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. ![CleanShot 2023-12-03 at 12 36 59](https://github.com/symfony/ux/assets/80641364/28bc4b75-215c-4588-80b2-0db9eb82dcc6) While implementing UX's Autocomplete in Sylius, I wanted to complete the following user story ``` Given I want to edit a taxon When I want to assign a parent taxon to it And I check a list of available taxa Then I should see all taxa except the edited one ``` 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 special `extra_options` key, which is an array consisting `scalars`/`arrays`/`nulls`. Next, when the form is rendered, I get these values, calculate a checksum for them, then pass them through `json_encode` and `base64_encode` functions. In the end, I glue them to the `url` values in the `\Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension::finishView` method. So, basically with the following configuration: ![CleanShot 2023-12-03 at 12 48 51](https://github.com/symfony/ux/assets/80641364/e7b2585c-f052-4803-9473-a83930a4d1c8) we end up with the following HTML code: ![CleanShot 2023-12-03 at 12 49 36](https://github.com/symfony/ux/assets/80641364/a173220f-19ed-4104-951c-0fee3469768e) 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. Commits ------- 308daef [Autocomplete] Allow passing extra options to the autocomplete fields
- Loading branch information
Showing
22 changed files
with
480 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\Autocomplete\Checksum; | ||
|
||
/** @internal */ | ||
class ChecksumCalculator | ||
{ | ||
public function __construct(private readonly string $secret) | ||
{ | ||
} | ||
|
||
public function calculateForArray(array $data): string | ||
{ | ||
$this->sortKeysRecursively($data); | ||
|
||
return base64_encode(hash_hmac('sha256', json_encode($data), $this->secret, true)); | ||
} | ||
|
||
private function sortKeysRecursively(array &$data): void | ||
{ | ||
foreach ($data as &$value) { | ||
if (\is_array($value)) { | ||
$this->sortKeysRecursively($value); | ||
} | ||
} | ||
ksort($data); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,20 +14,27 @@ | |
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||
use Symfony\UX\Autocomplete\AutocompleteResultsExecutor; | ||
use Symfony\UX\Autocomplete\AutocompleterRegistry; | ||
use Symfony\UX\Autocomplete\Checksum\ChecksumCalculator; | ||
use Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension; | ||
use Symfony\UX\Autocomplete\OptionsAwareEntityAutocompleterInterface; | ||
|
||
/** | ||
* @author Ryan Weaver <[email protected]> | ||
*/ | ||
final class EntityAutocompleteController | ||
{ | ||
public const EXTRA_OPTIONS = 'extra_options'; | ||
|
||
public function __construct( | ||
private AutocompleterRegistry $autocompleteFieldRegistry, | ||
private AutocompleteResultsExecutor $autocompleteResultsExecutor, | ||
private UrlGeneratorInterface $urlGenerator, | ||
private ChecksumCalculator $checksumCalculator, | ||
) { | ||
} | ||
|
||
|
@@ -38,6 +45,11 @@ public function __invoke(string $alias, Request $request): Response | |
throw new NotFoundHttpException(sprintf('No autocompleter found for "%s". Available autocompleters are: (%s)', $alias, implode(', ', $this->autocompleteFieldRegistry->getAutocompleterNames()))); | ||
} | ||
|
||
if ($autocompleter instanceof OptionsAwareEntityAutocompleterInterface) { | ||
$extraOptions = $this->getExtraOptions($request); | ||
$autocompleter->setOptions([self::EXTRA_OPTIONS => $extraOptions]); | ||
} | ||
|
||
$page = $request->query->getInt('page', 1); | ||
$nextPage = null; | ||
|
||
|
@@ -54,4 +66,48 @@ public function __invoke(string $alias, Request $request): Response | |
'next_page' => $nextPage, | ||
]); | ||
} | ||
|
||
/** | ||
* @return array<string, scalar|array|null> | ||
*/ | ||
private function getExtraOptions(Request $request): array | ||
{ | ||
if (!$request->query->has(self::EXTRA_OPTIONS)) { | ||
return []; | ||
} | ||
|
||
$extraOptions = $this->getDecodedExtraOptions($request->query->getString(self::EXTRA_OPTIONS)); | ||
|
||
if (!\array_key_exists(AutocompleteChoiceTypeExtension::CHECKSUM_KEY, $extraOptions)) { | ||
throw new BadRequestHttpException('The extra options are missing the checksum.'); | ||
} | ||
|
||
$this->validateChecksum($extraOptions[AutocompleteChoiceTypeExtension::CHECKSUM_KEY], $extraOptions); | ||
|
||
return $extraOptions; | ||
} | ||
|
||
/** | ||
* @return array<string, scalar> | ||
*/ | ||
private function getDecodedExtraOptions(string $extraOptions): array | ||
{ | ||
return json_decode(base64_decode($extraOptions), true, flags: \JSON_THROW_ON_ERROR); | ||
} | ||
|
||
/** | ||
* @param array<string, scalar> $extraOptions | ||
*/ | ||
private function validateChecksum(string $checksum, array $extraOptions): void | ||
{ | ||
$extraOptionsWithoutChecksum = array_filter( | ||
$extraOptions, | ||
fn (string $key) => AutocompleteChoiceTypeExtension::CHECKSUM_KEY !== $key, | ||
\ARRAY_FILTER_USE_KEY, | ||
); | ||
|
||
if ($checksum !== $this->checksumCalculator->calculateForArray($extraOptionsWithoutChecksum)) { | ||
throw new BadRequestHttpException('The extra options have been tampered with.'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/Autocomplete/src/OptionsAwareEntityAutocompleterInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\Autocomplete; | ||
|
||
/** | ||
* Interface for classes that will have an "autocomplete" endpoint exposed with a possibility to pass additional form options. | ||
*/ | ||
interface OptionsAwareEntityAutocompleterInterface extends EntityAutocompleterInterface | ||
{ | ||
public function setOptions(array $options): void; | ||
} |
17 changes: 9 additions & 8 deletions
17
src/Autocomplete/tests/Fixtures/Autocompleter/CustomGroupByProductAutocompleter.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,15 @@ | ||
<?php | ||
|
||
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter; | ||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
use Doctrine\ORM\EntityRepository; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Bundle\SecurityBundle\Security; | ||
use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil; | ||
use Symfony\UX\Autocomplete\EntityAutocompleterInterface; | ||
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product; | ||
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter; | ||
|
||
class CustomGroupByProductAutocompleter extends CustomProductAutocompleter | ||
{ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,20 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter; | ||
|
||
use Doctrine\ORM\EntityRepository; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Bundle\SecurityBundle\Security; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil; | ||
use Symfony\UX\Autocomplete\EntityAutocompleterInterface; | ||
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product; | ||
|
@@ -14,9 +23,8 @@ class CustomProductAutocompleter implements EntityAutocompleterInterface | |
{ | ||
public function __construct( | ||
private RequestStack $requestStack, | ||
private EntitySearchUtil $entitySearchUtil | ||
) | ||
{ | ||
private EntitySearchUtil $entitySearchUtil, | ||
) { | ||
} | ||
|
||
public function getEntityClass(): string | ||
|
Oops, something went wrong.