Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Autocomplete/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## Unreleased

- Translate the `optgroup` labels returned by the AJAX endpoint, so that the `group_by`
option can use translation keys, like the ones rendered by the form theme already do.
The `getTranslationDomain()` method can be implemented by custom autocompleters to
choose the domain; entity autocomplete fields use their `choice_translation_domain` option.

## 2.36.2

- Fix the autocomplete search query throwing an exception on PostgreSQL because of
Expand Down
4 changes: 3 additions & 1 deletion src/Autocomplete/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-foundation": "^6.3|^7.0|^8.0",
"symfony/http-kernel": "^6.3|^7.0|^8.0",
"symfony/property-access": "^6.3|^7.0|^8.0"
"symfony/property-access": "^6.3|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3"
},
"require-dev": {
"doctrine/collections": "^1.6.8|^2.0",
Expand All @@ -44,6 +45,7 @@
"symfony/phpunit-bridge": "^6.3|^7.0|^8.0",
"symfony/process": "^6.3|^7.0|^8.0",
"symfony/security-bundle": "^6.3|^7.0|^8.0",
"symfony/translation": "^6.3|^7.0|^8.0",
"symfony/twig-bundle": "^6.3|^7.0|^8.0",
"symfony/uid": "^6.3|^7.0|^8.0",
"twig/twig": "^2.14.7|^3.0.4",
Expand Down
43 changes: 36 additions & 7 deletions src/Autocomplete/src/AutocompleteResultsExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\Autocomplete\Doctrine\DoctrineRegistryWrapper;

/**
Expand All @@ -28,19 +30,22 @@ final class AutocompleteResultsExecutor
{
private PropertyAccessorInterface $propertyAccessor;
private ?Security $security;
private ?TranslatorInterface $translator;

public function __construct(
private DoctrineRegistryWrapper $managerRegistry,
$propertyAccessor,
/* Security $security = null */
/* Security $security = null, TranslatorInterface $translator = null */
) {
if ($propertyAccessor instanceof Security) {
trigger_deprecation('symfony/ux-autocomplete', '2.8.0', 'Passing a "%s" instance as the second argument of "%s()" is deprecated, pass a "%s" instance instead.', Security::class, __METHOD__, PropertyAccessorInterface::class);
$this->security = $propertyAccessor;
$this->propertyAccessor = new PropertyAccessor();
$this->translator = \func_num_args() >= 3 ? func_get_arg(2) : null;
} else {
$this->propertyAccessor = $propertyAccessor;
$this->security = \func_num_args() >= 3 ? func_get_arg(2) : null;
$this->translator = \func_num_args() >= 4 ? func_get_arg(3) : null;
}
}

Expand Down Expand Up @@ -98,25 +103,49 @@ public function fetchResults(EntityAutocompleterInterface $autocompleter, string
throw new \InvalidArgumentException(\sprintf('Option "group_by" must be callable, "%s" given.', get_debug_type($groupBy)));
}

$optgroupLabels = [];
$translationDomain = method_exists($autocompleter, 'getTranslationDomain') ? $autocompleter->getTranslationDomain() : null;

$optgroups = [];

foreach ($paginator as $entity) {
$result = $this->formatResult($autocompleter, $entity);

$groupLabels = $groupBy($entity, $result['value'], $result['text']);

if (null !== $groupLabels) {
$groupLabels = \is_array($groupLabels) ? array_map('strval', $groupLabels) : [(string) $groupLabels];
$result['group_by'] = $groupLabels;
$optgroupLabels = array_merge($optgroupLabels, $groupLabels);
$groupLabels = \is_array($groupLabels) ? $groupLabels : [$groupLabels];
$groupValues = [];

foreach ($groupLabels as $groupLabel) {
$label = $this->translateGroupLabel($groupLabel, $translationDomain);
// the value ties the results to their optgroup: keep the untranslated
// one whenever it is available, so that it does not depend on the locale
$value = $groupLabel instanceof TranslatableInterface ? $label : (string) $groupLabel;

$groupValues[] = $value;
$optgroups[$value] ??= ['value' => $value, 'label' => $label];
}

$result['group_by'] = $groupValues;
}

$results[] = $result;
}

$optgroups = array_map(static fn (string $label) => ['value' => $label, 'label' => $label], array_unique($optgroupLabels));
return new AutocompleteResults($results, $hasNextPage, array_values($optgroups));
}

private function translateGroupLabel(mixed $groupLabel, string|false|null $translationDomain): string
{
if (null === $this->translator || false === $translationDomain) {
return (string) $groupLabel;
}

if ($groupLabel instanceof TranslatableInterface) {
return $groupLabel->trans($this->translator);
}

return new AutocompleteResults($results, $hasNextPage, $optgroups);
return $this->translator->trans((string) $groupLabel, [], $translationDomain);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private function registerBasicServices(ContainerBuilder $container): void
new Reference('ux.autocomplete.doctrine_registry_wrapper'),
new Reference('property_accessor'),
new Reference('security.helper', ContainerInterface::NULL_ON_INVALID_REFERENCE),
new Reference('translator', ContainerInterface::NULL_ON_INVALID_REFERENCE),
])
;

Expand Down
14 changes: 12 additions & 2 deletions src/Autocomplete/src/EntityAutocompleterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
*
* TODO Remove next lines for Symfony UX 3
*
* @method array getAttributes(object $entity) Returns extra attributes to add to the autocomplete result.
* @method mixed getGroupBy() Return group_by option.
* @method array getAttributes(object $entity) Returns extra attributes to add to the autocomplete result.
* @method mixed getGroupBy() Return group_by option.
* @method string|false|null getTranslationDomain() Return the translation domain used for the "group_by" labels.
*/
interface EntityAutocompleterInterface
{
Expand Down Expand Up @@ -75,4 +76,13 @@ public function isGranted(Security $security): bool;
* TODO Uncomment for Symfony UX 3
*/
/* public function getGroupBy(): mixed; */

/*
* Return the translation domain used for the "group_by" labels.
*
* Returning null uses the default domain, false disables the translation.
*
* TODO Uncomment for Symfony UX 3
*/
/* public function getTranslationDomain(): string|false|null; */
}
8 changes: 8 additions & 0 deletions src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ public function getGroupBy(): mixed
return $this->getFormOption('group_by');
}

public function getTranslationDomain(): string|false|null
{
// the "choice_translation_domain" option is normalized to the form
// "translation_domain" when it is true, and to false when translation
// is disabled
return $this->getFormOption('choice_translation_domain');
}

private function getFormOption(string $name): mixed
{
$form = $this->getForm();
Expand Down
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 <fabien@symfony.com>
*
* 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;

class CustomGroupByTranslatedProductAutocompleter extends CustomGroupByProductAutocompleter
{
public function getTranslationDomain(): string|false|null
{
return 'autocomplete';
}
}
12 changes: 12 additions & 0 deletions src/Autocomplete/tests/Fixtures/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Symfony\UX\Autocomplete\DependencyInjection\AutocompleteFormTypePass;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomAttributesProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomGroupByProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomGroupByTranslatedProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Form\CategoryWithCallbackAsCustomValue;
use Symfony\UX\Autocomplete\Tests\Fixtures\Form\CategoryWithPropertyNameAsCustomValue;
Expand Down Expand Up @@ -102,6 +103,10 @@ protected function configureContainer(ContainerConfigurator $c): void
'secrets' => false,
'session' => ['storage_factory_id' => 'session.storage.factory.mock_file'],
'form' => ['enabled' => $this->enableForms],
'translator' => [
'default_path' => '%kernel.project_dir%/tests/Fixtures/translations',
'fallbacks' => ['en'],
],
]);

$c->extension('twig', [
Expand Down Expand Up @@ -191,6 +196,13 @@ protected function configureContainer(ContainerConfigurator $c): void
'alias' => 'custom_group_by_product',
]);

$services->set(CustomGroupByTranslatedProductAutocompleter::class)
->public()
->arg(1, new Reference('ux.autocomplete.entity_search_util'))
->tag(AutocompleteFormTypePass::ENTITY_AUTOCOMPLETER_TAG, [
'alias' => 'custom_group_by_translated_product',
]);

$services->set(CustomAttributesProductAutocompleter::class)
->public()
->arg(1, new Reference('ux.autocomplete.entity_search_util'))
Expand Down
15 changes: 15 additions & 0 deletions src/Autocomplete/tests/Fixtures/translations/autocomplete.en.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

return [
'foods' => 'Food & drinks',
'toys' => 'Toys',
];
34 changes: 33 additions & 1 deletion src/Autocomplete/tests/Integration/WiringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\UX\Autocomplete\AutocompleteResultsExecutor;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomGroupByProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomGroupByTranslatedProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomProductAutocompleter;
use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\CategoryFactory;
use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\ProductFactory;
Expand Down Expand Up @@ -93,6 +94,37 @@ public function testWiringWithoutFormAndGroupByOption()
$autocompleter = $kernel->getContainer()->get(CustomGroupByProductAutocompleter::class);
$data = $executor->fetchResults($autocompleter, '', 1);
$this->assertCount(3, $data->results);
$this->assertCount(2, $data->optgroups);
$this->assertSame([
['value' => 'foods', 'label' => 'foods'],
['value' => 'toys', 'label' => 'toys'],
], $data->optgroups);
}

public function testWiringWithoutFormAndTranslatedGroupByOption()
{
$kernel = new Kernel('test', true);
$kernel->disableForms();
$kernel->boot();

$category1 = CategoryFactory::createOne(['name' => 'foods']);
$category2 = CategoryFactory::createOne(['name' => 'toys']);
ProductFactory::createOne(['name' => 'pizza', 'category' => $category1]);
ProductFactory::createOne(['name' => 'toy food', 'category' => $category2]);
ProductFactory::createOne(['name' => 'puzzle', 'category' => $category2]);

/** @var AutocompleteResultsExecutor $executor */
$executor = $kernel->getContainer()->get('public.results_executor');
$autocompleter = $kernel->getContainer()->get(CustomGroupByTranslatedProductAutocompleter::class);
$data = $executor->fetchResults($autocompleter, '', 1);

$this->assertCount(3, $data->results);
// the label is translated, while the value stays untranslated so that it
// keeps tying the results to their optgroup whatever the locale is
$this->assertSame([
['value' => 'foods', 'label' => 'Food & drinks'],
['value' => 'toys', 'label' => 'Toys'],
], $data->optgroups);
$this->assertSame(['foods'], $data->results[0]['group_by']);
$this->assertSame(['toys'], $data->results[1]['group_by']);
}
}
Loading