diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index e0cd19afd52ae..1d8689806912c 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * added support for translating `Translatable` objects * added the `t()` function to easily create `Translatable` objects * Added support for extracting messages from the `t()` function + * Added `field_*` Twig functions to access string values from Form fields 5.0.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 0f4076db53275..080a5eb06e57b 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -12,8 +12,11 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormView; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -27,6 +30,13 @@ */ final class FormExtension extends AbstractExtension { + private $translator; + + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + /** * {@inheritdoc} */ @@ -55,6 +65,12 @@ public function getFunctions(): array new TwigFunction('form_end', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']]), new TwigFunction('csrf_token', ['Symfony\Component\Form\FormRenderer', 'renderCsrfToken']), new TwigFunction('form_parent', 'Symfony\Bridge\Twig\Extension\twig_get_form_parent'), + new TwigFunction('field_name', [$this, 'getFieldName']), + new TwigFunction('field_value', [$this, 'getFieldValue']), + new TwigFunction('field_label', [$this, 'getFieldLabel']), + new TwigFunction('field_help', [$this, 'getFieldHelp']), + new TwigFunction('field_errors', [$this, 'getFieldErrors']), + new TwigFunction('field_choices', [$this, 'getFieldChoices']), ]; } @@ -79,6 +95,80 @@ public function getTests(): array new TwigTest('rootform', 'Symfony\Bridge\Twig\Extension\twig_is_root_form'), ]; } + + public function getFieldName(FormView $view): string + { + $view->setRendered(); + + return $view->vars['full_name']; + } + + public function getFieldValue(FormView $view): string + { + return $view->vars['value']; + } + + public function getFieldLabel(FormView $view): string + { + return $this->createFieldTranslation( + $view->vars['label'], + $view->vars['label_translation_parameters'] ?: [], + $view->vars['translation_domain'] + ); + } + + public function getFieldHelp(FormView $view): string + { + return $this->createFieldTranslation( + $view->vars['help'], + $view->vars['help_translation_parameters'] ?: [], + $view->vars['translation_domain'] + ); + } + + /** + * @return string[] + */ + public function getFieldErrors(FormView $view): iterable + { + /** @var FormError $error */ + foreach ($view->vars['errors'] as $error) { + yield $error->getMessage(); + } + } + + /** + * @return string[]|string[][] + */ + public function getFieldChoices(FormView $view): iterable + { + yield from $this->createFieldChoicesList($view->vars['choices'], $view->vars['choice_translation_domain']); + } + + private function createFieldChoicesList(iterable $choices, $translationDomain): iterable + { + foreach ($choices as $choice) { + $translatableLabel = $this->createFieldTranslation($choice->label, [], $translationDomain); + + if ($choice instanceof ChoiceGroupView) { + yield $translatableLabel => $this->createFieldChoicesList($choice, $translationDomain); + + continue; + } + + /* @var ChoiceView $choice */ + yield $translatableLabel => $choice->value; + } + } + + private function createFieldTranslation(?string $value, array $parameters, $domain): string + { + if (!$this->translator || !$value || false === $domain) { + return $value; + } + + return $this->translator->trans($value, $parameters, $domain); + } } /** diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php new file mode 100644 index 0000000000000..f773c1b430918 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php @@ -0,0 +1,237 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Test\FormIntegrationTestCase; + +class FormExtensionFieldHelpersTest extends FormIntegrationTestCase +{ + /** + * @var FormExtension + */ + private $rawExtension; + + /** + * @var FormExtension + */ + private $translatorExtension; + + /** + * @var FormView + */ + private $view; + + protected function getTypes() + { + return [new TextType(), new ChoiceType()]; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->rawExtension = new FormExtension(); + $this->translatorExtension = new FormExtension(new StubTranslator()); + + $form = $this->factory->createNamedBuilder('register', FormType::class, ['username' => 'tgalopin']) + ->add('username', TextType::class, [ + 'label' => 'base.username', + 'label_translation_parameters' => ['%label_brand%' => 'Symfony'], + 'help' => 'base.username_help', + 'help_translation_parameters' => ['%help_brand%' => 'Symfony'], + 'translation_domain' => 'forms', + ]) + ->add('choice_flat', ChoiceType::class, [ + 'choices' => [ + 'base.yes' => 'yes', + 'base.no' => 'no', + ], + 'choice_translation_domain' => 'forms', + ]) + ->add('choice_grouped', ChoiceType::class, [ + 'choices' => [ + 'base.europe' => [ + 'base.fr' => 'fr', + 'base.de' => 'de', + ], + 'base.asia' => [ + 'base.cn' => 'cn', + 'base.jp' => 'jp', + ], + ], + 'choice_translation_domain' => 'forms', + ]) + ->getForm() + ; + + $form->get('username')->addError(new FormError('username.max_length')); + + $this->view = $form->createView(); + } + + public function testFieldName() + { + $this->assertFalse($this->view->children['username']->isRendered()); + $this->assertSame('register[username]', $this->rawExtension->getFieldName($this->view->children['username'])); + $this->assertTrue($this->view->children['username']->isRendered()); + } + + public function testFieldValue() + { + $this->assertSame('tgalopin', $this->rawExtension->getFieldValue($this->view->children['username'])); + } + + public function testFieldLabel() + { + $this->assertSame('base.username', $this->rawExtension->getFieldLabel($this->view->children['username'])); + } + + public function testFieldTranslatedLabel() + { + $this->assertSame('[trans]base.username[/trans]', $this->translatorExtension->getFieldLabel($this->view->children['username'])); + } + + public function testFieldHelp() + { + $this->assertSame('base.username_help', $this->rawExtension->getFieldHelp($this->view->children['username'])); + } + + public function testFieldTranslatedHelp() + { + $this->assertSame('[trans]base.username_help[/trans]', $this->translatorExtension->getFieldHelp($this->view->children['username'])); + } + + public function testFieldErrors() + { + $errors = $this->rawExtension->getFieldErrors($this->view->children['username']); + $this->assertSame(['username.max_length'], iterator_to_array($errors)); + } + + public function testFieldTranslatedErrors() + { + $errors = $this->translatorExtension->getFieldErrors($this->view->children['username']); + $this->assertSame(['username.max_length'], iterator_to_array($errors)); + } + + public function testFieldChoicesFlat() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_flat']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('yes', $choicesArray[0]['value']); + $this->assertSame('base.yes', $choicesArray[0]['label']); + + $this->assertSame('no', $choicesArray[1]['value']); + $this->assertSame('base.no', $choicesArray[1]['label']); + } + + public function testFieldTranslatedChoicesFlat() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_flat']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('yes', $choicesArray[0]['value']); + $this->assertSame('[trans]base.yes[/trans]', $choicesArray[0]['label']); + + $this->assertSame('no', $choicesArray[1]['value']); + $this->assertSame('[trans]base.no[/trans]', $choicesArray[1]['label']); + } + + public function testFieldChoicesGrouped() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_grouped']); + + $choicesArray = []; + foreach ($choices as $groupLabel => $groupChoices) { + $groupChoicesArray = []; + foreach ($groupChoices as $label => $value) { + $groupChoicesArray[] = ['label' => $label, 'value' => $value]; + } + + $choicesArray[] = ['label' => $groupLabel, 'choices' => $groupChoicesArray]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertCount(2, $choicesArray[0]['choices']); + $this->assertSame('base.europe', $choicesArray[0]['label']); + + $this->assertSame('fr', $choicesArray[0]['choices'][0]['value']); + $this->assertSame('base.fr', $choicesArray[0]['choices'][0]['label']); + + $this->assertSame('de', $choicesArray[0]['choices'][1]['value']); + $this->assertSame('base.de', $choicesArray[0]['choices'][1]['label']); + + $this->assertCount(2, $choicesArray[1]['choices']); + $this->assertSame('base.asia', $choicesArray[1]['label']); + + $this->assertSame('cn', $choicesArray[1]['choices'][0]['value']); + $this->assertSame('base.cn', $choicesArray[1]['choices'][0]['label']); + + $this->assertSame('jp', $choicesArray[1]['choices'][1]['value']); + $this->assertSame('base.jp', $choicesArray[1]['choices'][1]['label']); + } + + public function testFieldTranslatedChoicesGrouped() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_grouped']); + + $choicesArray = []; + foreach ($choices as $groupLabel => $groupChoices) { + $groupChoicesArray = []; + foreach ($groupChoices as $label => $value) { + $groupChoicesArray[] = ['label' => $label, 'value' => $value]; + } + + $choicesArray[] = ['label' => $groupLabel, 'choices' => $groupChoicesArray]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertCount(2, $choicesArray[0]['choices']); + $this->assertSame('[trans]base.europe[/trans]', $choicesArray[0]['label']); + + $this->assertSame('fr', $choicesArray[0]['choices'][0]['value']); + $this->assertSame('[trans]base.fr[/trans]', $choicesArray[0]['choices'][0]['label']); + + $this->assertSame('de', $choicesArray[0]['choices'][1]['value']); + $this->assertSame('[trans]base.de[/trans]', $choicesArray[0]['choices'][1]['label']); + + $this->assertCount(2, $choicesArray[1]['choices']); + $this->assertSame('[trans]base.asia[/trans]', $choicesArray[1]['label']); + + $this->assertSame('cn', $choicesArray[1]['choices'][0]['value']); + $this->assertSame('[trans]base.cn[/trans]', $choicesArray[1]['choices'][0]['label']); + + $this->assertSame('jp', $choicesArray[1]['choices'][1]['value']); + $this->assertSame('[trans]base.jp[/trans]', $choicesArray[1]['choices'][1]['label']); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/form.php b/src/Symfony/Bundle/TwigBundle/Resources/config/form.php index bbc1b51a9c296..9f2efdf94105c 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/form.php +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/form.php @@ -18,6 +18,7 @@ return static function (ContainerConfigurator $container) { $container->services() ->set('twig.extension.form', FormExtension::class) + ->args([service('translator')->nullOnInvalid()]) ->set('twig.form.engine', TwigRendererEngine::class) ->args([param('twig.form.resources'), service('twig')])