From 1ef518aecd10af3db9ae567e5dea6cad9a909283 Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 27 Dec 2020 13:04:19 +0100 Subject: [PATCH] RenderElement test & small fixes in extension classes --- src/Resources/config/twig.php | 3 +- src/Twig/Extension/CanonicalizeExtension.php | 7 +- src/Twig/Extension/RenderElementExtension.php | 10 +- src/Twig/Extension/SecurityExtension.php | 3 + src/Twig/Extension/SonataAdminExtension.php | 59 +- src/Twig/Extension/XEditableExtension.php | 10 + tests/Controller/HelperControllerTest.php | 4 +- .../Extension/RenderElementExtensionTest.php | 2538 +++++++++++++++++ .../Extension/SonataAdminExtensionTest.php | 2 +- 9 files changed, 2596 insertions(+), 40 deletions(-) create mode 100644 tests/Twig/Extension/RenderElementExtensionTest.php diff --git a/src/Resources/config/twig.php b/src/Resources/config/twig.php index 9588ccb9084..a4d29b2890c 100644 --- a/src/Resources/config/twig.php +++ b/src/Resources/config/twig.php @@ -62,6 +62,7 @@ // NEXT_MAJOR: Remove next line. new ReferenceConfigurator('security.authorization_checker'), ]) + // NEXT_MAJOR: Remove next call. ->call('setXEditableTypeMapping', [ '%sonata.admin.twig.extension.x_editable_type_mapping%', 'sonata_deprecation_mute', @@ -103,7 +104,7 @@ '%sonata.admin.twig.extension.x_editable_type_mapping%', ]) - ->set('sonata.renderElement.twig.extension', RenderElementExtension::class) + ->set('sonata.render_element.twig.extension', RenderElementExtension::class) ->tag('twig.extension') ->args([ new ReferenceConfigurator('property_accessor'), diff --git a/src/Twig/Extension/CanonicalizeExtension.php b/src/Twig/Extension/CanonicalizeExtension.php index 0ca0b301cf4..76644f2574b 100644 --- a/src/Twig/Extension/CanonicalizeExtension.php +++ b/src/Twig/Extension/CanonicalizeExtension.php @@ -32,6 +32,9 @@ final class CanonicalizeExtension extends AbstractExtension */ private $requestStack; + /** + * @internal This class should only be used through Twig + */ public function __construct(RequestStack $requestStack) { $this->requestStack = $requestStack; @@ -44,8 +47,8 @@ public function getFunctions(): array { return [ //NEXT_MAJOR: Uncomment lines below - //new TwigFunction('canonicalize_locale_for_moment', [$this, 'getCanonicalizedLocaleForMoment'], ['needs_context' => true]), - //new TwigFunction('canonicalize_locale_for_select2', [$this, 'getCanonicalizedLocaleForSelect2'], ['needs_context' => true]), + //new TwigFunction('canonicalize_locale_for_moment', [$this, 'getCanonicalizedLocaleForMoment']), + //new TwigFunction('canonicalize_locale_for_select2', [$this, 'getCanonicalizedLocaleForSelect2']), ]; } diff --git a/src/Twig/Extension/RenderElementExtension.php b/src/Twig/Extension/RenderElementExtension.php index be84585a3ca..595f9a6c310 100644 --- a/src/Twig/Extension/RenderElementExtension.php +++ b/src/Twig/Extension/RenderElementExtension.php @@ -44,8 +44,11 @@ final class RenderElementExtension extends AbstractExtension */ private $propertyAccessor; + /** + * @internal This class should only be used through Twig + */ public function __construct( - ?PropertyAccessorInterface $propertyAccessor = null, + PropertyAccessorInterface $propertyAccessor, ?ContainerInterface $templateRegistries = null, ?LoggerInterface $logger = null ) { @@ -385,10 +388,7 @@ public function getObjectAndValueFromListElement( return [$object, $value]; } - /** - * NEXT MAJOR: Make this method private. - */ - public function render( + private function render( FieldDescriptionInterface $fieldDescription, TemplateWrapper $template, array $parameters, diff --git a/src/Twig/Extension/SecurityExtension.php b/src/Twig/Extension/SecurityExtension.php index 8cfa40f33f8..085fe68d50d 100644 --- a/src/Twig/Extension/SecurityExtension.php +++ b/src/Twig/Extension/SecurityExtension.php @@ -26,6 +26,9 @@ final class SecurityExtension extends AbstractExtension */ private $securityChecker; + /** + * @internal This class should only be used through Twig + */ public function __construct( ?AuthorizationCheckerInterface $securityChecker = null ) { diff --git a/src/Twig/Extension/SonataAdminExtension.php b/src/Twig/Extension/SonataAdminExtension.php index 212d0c16b6f..66a6cbd90d8 100644 --- a/src/Twig/Extension/SonataAdminExtension.php +++ b/src/Twig/Extension/SonataAdminExtension.php @@ -388,12 +388,36 @@ public function output( array $parameters, Environment $environment ) { - return $this->render( - $fieldDescription, - new TemplateWrapper($environment, $template), - $parameters, - $environment - ); + @trigger_error(sprintf( + 'The %s method is deprecated since version 3.33 and will be removed in 4.0.', + __METHOD__ + ), E_USER_DEPRECATED); + + $content = $template->render($parameters); + + if ($environment->isDebug()) { + $commentTemplate = <<<'EOT' + + + %s + +EOT; + + return sprintf( + $commentTemplate, + $fieldDescription->getFieldName(), + $fieldDescription->getTemplate(), + $template->getSourceContext()->getName(), + $content, + $fieldDescription->getFieldName() + ); + } + + return $content; } /** @@ -731,29 +755,6 @@ protected function getTemplate( return $this->renderElementExtension->getTemplate($fieldDescription, $defaultTemplate, $$environment); } - /** - * NEXT_MAJOR: Remove this method. - */ - private function render( - FieldDescriptionInterface $fieldDescription, - TemplateWrapper $template, - array $parameters, - Environment $environment - ): string { - if ('sonata_deprecation_mute' !== (\func_get_args()[4] ?? null)) { - @trigger_error(sprintf( - 'The %s method is deprecated in favor of RenderElementExtension::render since version 3.x and will be removed in 4.0.', - __METHOD__ - ), E_USER_DEPRECATED); - } - - if (null === $this->renderElementExtension) { - $this->renderElementExtension = new RenderElementExtension($this->propertyAccessor, $this->templateRegistries, $this->logger); - } - - return $this->renderElementExtension->render($fieldDescription, $template, $parameters, $environment); - } - /** * NEXT_MAJOR: Remove this method * Extracts the object and requested value from the $listElement. diff --git a/src/Twig/Extension/XEditableExtension.php b/src/Twig/Extension/XEditableExtension.php index 07e014ae65a..3d5ba13fe8b 100644 --- a/src/Twig/Extension/XEditableExtension.php +++ b/src/Twig/Extension/XEditableExtension.php @@ -32,6 +32,8 @@ final class XEditableExtension extends AbstractExtension /** * @param string[] $xEditableTypeMapping + * + * @internal This class should only be used through Twig */ public function __construct( TranslatorInterface $translator, @@ -69,6 +71,14 @@ public function getXEditableType(string $type) return $this->xEditableTypeMapping[$type] ?? false; } + /** + * @param string[] $xEditableTypeMapping + */ + public function setXEditableTypeMapping($xEditableTypeMapping) + { + $this->xEditableTypeMapping = $xEditableTypeMapping; + } + /** * Return xEditable choices based on the field description choices options & catalogue options. * With the following choice options: diff --git a/tests/Controller/HelperControllerTest.php b/tests/Controller/HelperControllerTest.php index 48f7339005e..2aa6c3574be 100644 --- a/tests/Controller/HelperControllerTest.php +++ b/tests/Controller/HelperControllerTest.php @@ -224,7 +224,7 @@ public function testSetObjectFieldValueAction(): void $container->set('sonata.post.admin.template_registry', $templateRegistry); $this->pool->method('getPropertyAccessor')->willReturn($propertyAccessor); $this->twig->method('getExtension')->with(SonataAdminExtension::class)->willReturn( - new SonataAdminExtension($pool, null, $translator, $container) + new SonataAdminExtension($pool, null, $translator, $container, $propertyAccessor) ); $this->twig->method('load')->with('admin_template')->willReturn(new TemplateWrapper($this->twig, $template)); $this->twig->method('isDebug')->willReturn(false); @@ -280,7 +280,7 @@ public function testSetObjectFieldValueActionOnARelationField(): void $this->admin->method('getModelManager')->willReturn($modelManager); $this->validator->method('validate')->with($object)->willReturn(new ConstraintViolationList([])); $this->twig->method('getExtension')->with(SonataAdminExtension::class)->willReturn( - new SonataAdminExtension($this->pool, null, $translator, $container) + new SonataAdminExtension($this->pool, null, $translator, $container, $propertyAccessor) ); $this->twig->method('load')->with('field_template')->willReturn(new TemplateWrapper($this->twig, $template)); $this->twig->method('isDebug')->willReturn(false); diff --git a/tests/Twig/Extension/RenderElementExtensionTest.php b/tests/Twig/Extension/RenderElementExtensionTest.php new file mode 100644 index 00000000000..5e404c929d3 --- /dev/null +++ b/tests/Twig/Extension/RenderElementExtensionTest.php @@ -0,0 +1,2538 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\Twig\Extension; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Sonata\AdminBundle\Admin\AbstractAdmin; +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\FieldDescriptionInterface; +use Sonata\AdminBundle\Admin\Pool; +use Sonata\AdminBundle\Exception\NoValueException; +use Sonata\AdminBundle\Templating\TemplateRegistryInterface; +use Sonata\AdminBundle\Tests\Fixtures\Entity\FooToString; +use Sonata\AdminBundle\Tests\Fixtures\StubFilesystemLoader; +use Sonata\AdminBundle\Twig\Extension\RenderElementExtension; +use Sonata\AdminBundle\Twig\Extension\SonataAdminExtension; +use Sonata\AdminBundle\Twig\Extension\XEditableExtension; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Bridge\Twig\Extension\RoutingExtension; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\Loader\XmlFileLoader; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Translation\Loader\XliffFileLoader; +use Symfony\Component\Translation\Translator; +use Symfony\Contracts\Translation\TranslatorInterface; +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Extra\String\StringExtension; + +/** + * @author Andrej Hudec + */ +final class RenderElementExtensionTest extends TestCase +{ + use ExpectDeprecationTrait; + + /** + * @var RenderElementExtension + */ + private $twigExtension; + + /** + * @var Environment + */ + private $environment; + + /** + * @var AdminInterface + */ + private $admin; + + /** + * @var AdminInterface + */ + private $adminBar; + + /** + * @var FieldDescriptionInterface + */ + private $fieldDescription; + + /** + * @var \stdClass + */ + private $object; + + /** + * @var Pool + */ + private $pool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var Container + */ + private $container; + + /** + * @var TemplateRegistryInterface + */ + private $templateRegistry; + + /** + * @var PropertyAccessor + */ + private $propertyAccessor; + + protected function setUp(): void + { + date_default_timezone_set('Europe/London'); + + $xEditableTypeMapping = [ + 'choice' => 'select', + 'boolean' => 'select', + 'text' => 'text', + 'textarea' => 'textarea', + 'html' => 'textarea', + 'email' => 'email', + 'string' => 'text', + 'smallint' => 'text', + 'bigint' => 'text', + 'integer' => 'number', + 'decimal' => 'number', + 'currency' => 'number', + 'percent' => 'number', + 'url' => 'url', + ]; + + $container = new Container(); + + $this->pool = new Pool($container, '', ''); + $this->pool->setAdminServiceIds(['sonata_admin_foo_service']); + $this->pool->setAdminClasses(['fooClass' => ['sonata_admin_foo_service']]); + + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + // translation extension + $translator = new Translator('en'); + $translator->addLoader('xlf', new XliffFileLoader()); + $translator->addResource( + 'xlf', + sprintf('%s/../../../src/Resources/translations/SonataAdminBundle.en.xliff', __DIR__), + 'en', + 'SonataAdminBundle' + ); + + $this->translator = $translator; + + $this->templateRegistry = $this->createStub(TemplateRegistryInterface::class); + $this->container = new Container(); + $this->container->set('sonata_admin_foo_service.template_registry', $this->templateRegistry); + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + + $request = $this->createMock(Request::class); + $request->method('get')->with('_sonata_admin')->willReturn('sonata_admin_foo_service'); + + $loader = new StubFilesystemLoader([ + __DIR__.'/../../../src/Resources/views/CRUD', + __DIR__.'/../../Fixtures/Resources/views/CRUD', + ]); + $loader->addPath(__DIR__.'/../../../src/Resources/views/', 'SonataAdmin'); + $loader->addPath(__DIR__.'/../../Fixtures/Resources/views/', 'App'); + + $this->environment = new Environment($loader, [ + 'strict_variables' => true, + 'cache' => false, + 'autoescape' => 'html', + 'optimizations' => 0, + ]); + + //NEXT_MAJOR: Remove follwing block + /** + * @var AuthorizationCheckerInterface + */ + $securityChecker = $this->createStub(AuthorizationCheckerInterface::class); + $this->twigExtension = new SonataAdminExtension( + $this->pool, + $this->logger, + $this->translator, + $this->container, + $propertyAccessor, + $securityChecker + ); + $this->twigExtension->setXEditableTypeMapping($xEditableTypeMapping, 'sonata_deprecation_mute'); + $this->environment->addExtension($this->twigExtension); + // block ends + + //NEXT_MAJOR: Uncomment block below + /* + $this->twigExtension = new RenderElementExtension( + $propertyAccessor, + $this->container, + $this->logger, + ); + $this->environment->addExtension($this->twigExtension); + // xeditable extension + $xEditableExtension = new XEditableExtension($translator, $xEditableTypeMapping); + $xEditableExtension->setXEditableTypeMapping($xEditableTypeMapping); + $this->environment->addExtension($xEditableExtension); + */ + + $this->environment->addExtension(new TranslationExtension($translator)); + $this->environment->addExtension(new FakeTemplateRegistryExtension()); + + // routing extension + $xmlFileLoader = new XmlFileLoader(new FileLocator([sprintf('%s/../../../src/Resources/config/routing', __DIR__)])); + $routeCollection = $xmlFileLoader->load('sonata_admin.xml'); + + $xmlFileLoader = new XmlFileLoader(new FileLocator([sprintf('%s/../../Fixtures/Resources/config/routing', __DIR__)])); + + $testRouteCollection = $xmlFileLoader->load('routing.xml'); + + $routeCollection->addCollection($testRouteCollection); + $requestContext = new RequestContext(); + $urlGenerator = new UrlGenerator($routeCollection, $requestContext); + $this->environment->addExtension(new RoutingExtension($urlGenerator)); + $this->environment->addExtension(new StringExtension()); + + // initialize object + $this->object = new \stdClass(); + + // initialize admin + $this->admin = $this->createMock(AbstractAdmin::class); + + $this->admin + ->method('getCode') + ->willReturn('sonata_admin_foo_service'); + + $this->admin + ->method('id') + ->with($this->equalTo($this->object)) + ->willReturn(12345); + + $this->admin + ->method('getNormalizedIdentifier') + ->with($this->equalTo($this->object)) + ->willReturn(12345); + + $this->admin + ->method('trans') + ->willReturnCallback(static function ($id, $parameters = [], $domain = null) use ($translator) { + return $translator->trans($id, $parameters, $domain); + }); + + $this->adminBar = $this->createMock(AbstractAdmin::class); + $this->adminBar + ->method('hasAccess') + ->willReturn(true); + $this->adminBar + ->method('getNormalizedIdentifier') + ->with($this->equalTo($this->object)) + ->willReturn(12345); + + $container->set('sonata_admin_foo_service', $this->admin); + $container->set('sonata_admin_bar_service', $this->adminBar); + + // initialize field description + $this->fieldDescription = $this->getMockForAbstractClass(FieldDescriptionInterface::class); + + $this->fieldDescription + ->method('getName') + ->willReturn('fd_name'); + + $this->fieldDescription + ->method('getAdmin') + ->willReturn($this->admin); + + $this->fieldDescription + ->method('getLabel') + ->willReturn('Data'); + } + + /** + * @group legacy + * @expectedDeprecation The Sonata\AdminBundle\Admin\AbstractAdmin::getTemplate method is deprecated (since sonata-project/admin-bundle 3.34, will be dropped in 4.0. Use TemplateRegistry services instead). + * @dataProvider getRenderListElementTests + */ + public function testRenderListElement(string $expected, string $type, $value, array $options): void + { + $this->admin + ->method('getPersistentParameters') + ->willReturn(['context' => 'foo']); + + $this->admin + ->method('hasAccess') + ->willReturn(true); + + // NEXT_MAJOR: Remove this line + $this->admin + ->method('getTemplate') + ->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->templateRegistry->method('getTemplate')->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->fieldDescription + ->method('getValue') + ->willReturn($value); + + $this->fieldDescription + ->method('getType') + ->willReturn($type); + + $this->fieldDescription + ->method('getOptions') + ->willReturn($options); + + $this->fieldDescription + ->method('getOption') + ->willReturnCallback(static function ($name, $default = null) use ($options) { + return $options[$name] ?? $default; + }); + + $this->fieldDescription + ->method('getTemplate') + ->willReturnCallback(static function () use ($type): ?string { + switch ($type) { + case 'string': + return '@SonataAdmin/CRUD/list_string.html.twig'; + case 'boolean': + return '@SonataAdmin/CRUD/list_boolean.html.twig'; + case 'datetime': + return '@SonataAdmin/CRUD/list_datetime.html.twig'; + case 'date': + return '@SonataAdmin/CRUD/list_date.html.twig'; + case 'time': + return '@SonataAdmin/CRUD/list_time.html.twig'; + case 'currency': + return '@SonataAdmin/CRUD/list_currency.html.twig'; + case 'percent': + return '@SonataAdmin/CRUD/list_percent.html.twig'; + case 'email': + return '@SonataAdmin/CRUD/list_email.html.twig'; + case 'choice': + return '@SonataAdmin/CRUD/list_choice.html.twig'; + case 'array': + return '@SonataAdmin/CRUD/list_array.html.twig'; + case 'trans': + return '@SonataAdmin/CRUD/list_trans.html.twig'; + case 'url': + return '@SonataAdmin/CRUD/list_url.html.twig'; + case 'html': + return '@SonataAdmin/CRUD/list_html.html.twig'; + case 'nonexistent': + // template doesn`t exist + return '@SonataAdmin/CRUD/list_nonexistent_template.html.twig'; + default: + return null; + } + }); + + $this->assertSame( + $this->removeExtraWhitespace($expected), + $this->removeExtraWhitespace($this->twigExtension->renderListElement( + $this->environment, + $this->object, + $this->fieldDescription, + )) + ); + } + + /** + * NEXT_MAJOR: Remove @expectedDeprecation. + * + * @group legacy + * @expectedDeprecation The Sonata\AdminBundle\Admin\AbstractAdmin::getTemplate method is deprecated (since sonata-project/admin-bundle 3.34, will be dropped in 4.0. Use TemplateRegistry services instead). + */ + public function testRenderListElementWithAdditionalValuesInArray(): void + { + // NEXT_MAJOR: Remove this line + $this->admin + ->method('getTemplate') + ->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->templateRegistry->method('getTemplate')->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->fieldDescription + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/list_string.html.twig'); + + $this->assertSame( + $this->removeExtraWhitespace(' Extra value '), + $this->removeExtraWhitespace($this->twigExtension->renderListElement( + $this->environment, + [$this->object, 'fd_name' => 'Extra value'], + $this->fieldDescription + )) + ); + } + + /** + * NEXT_MAJOR: Remove @expectedDeprecation. + * + * @group legacy + * @expectedDeprecation Accessing a non existing value is deprecated since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0. + */ + public function testRenderListElementWithNoValueException(): void + { + // NEXT_MAJOR: Remove this line + $this->admin + ->method('getTemplate') + ->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->templateRegistry->method('getTemplate')->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->fieldDescription + ->method('getValue') + ->willReturnCallback(static function (): void { + throw new NoValueException(); + }); + + $this->assertSame( + $this->removeExtraWhitespace(' '), + $this->removeExtraWhitespace($this->twigExtension->renderListElement( + $this->environment, + $this->object, + $this->fieldDescription + )) + ); + } + + /** + * @dataProvider getDeprecatedRenderListElementTests + * @group legacy + */ + public function testDeprecatedRenderListElement(string $expected, ?string $value, array $options): void + { + $this->admin + ->method('hasAccess') + ->willReturn(true); + + // NEXT_MAJOR: Remove this line + $this->admin + ->method('getTemplate') + ->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->templateRegistry->method('getTemplate')->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->fieldDescription + ->method('getValue') + ->willReturn($value); + + $this->fieldDescription + ->method('getType') + ->willReturn('nonexistent'); + + $this->fieldDescription + ->method('getOptions') + ->willReturn($options); + + $this->fieldDescription + ->method('getOption') + ->willReturnCallback(static function ($name, $default = null) use ($options) { + return $options[$name] ?? $default; + }); + + $this->fieldDescription + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/list_nonexistent_template.html.twig'); + + $this->assertSame( + $this->removeExtraWhitespace($expected), + $this->removeExtraWhitespace($this->twigExtension->renderListElement( + $this->environment, + $this->object, + $this->fieldDescription + )) + ); + } + + /** + * @group legacy + */ + public function testRenderListElementNonExistentTemplate(): void + { + // NEXT_MAJOR: Remove this line + $this->admin->method('getTemplate') + ->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->templateRegistry->method('getTemplate')->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->fieldDescription->expects($this->once()) + ->method('getValue') + ->willReturn('Foo'); + + $this->fieldDescription->expects($this->once()) + ->method('getFieldName') + ->willReturn('Foo_name'); + + $this->fieldDescription->expects($this->exactly(2)) + ->method('getType') + ->willReturn('nonexistent'); + + $this->fieldDescription->expects($this->once()) + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/list_nonexistent_template.html.twig'); + + $this->logger->expects($this->once()) + ->method('warning') + ->with(($this->stringStartsWith($this->removeExtraWhitespace( + 'An error occured trying to load the template + "@SonataAdmin/CRUD/list_nonexistent_template.html.twig" + for the field "Foo_name", the default template + "@SonataAdmin/CRUD/base_list_field.html.twig" was used + instead.' + )))); + + $this->twigExtension->renderListElement($this->environment, $this->object, $this->fieldDescription); + } + + /** + * @group legacy + */ + public function testRenderListElementErrorLoadingTemplate(): void + { + $this->expectException(LoaderError::class); + $this->expectExceptionMessage('Unable to find template "@SonataAdmin/CRUD/base_list_nonexistent_field.html.twig"'); + + // NEXT_MAJOR: Remove this line + $this->admin->method('getTemplate') + ->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_nonexistent_field.html.twig'); + + $this->templateRegistry->method('getTemplate')->with('base_list_field') + ->willReturn('@SonataAdmin/CRUD/base_list_nonexistent_field.html.twig'); + + $this->fieldDescription->expects($this->once()) + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/list_nonexistent_template.html.twig'); + + $this->twigExtension->renderListElement($this->environment, $this->object, $this->fieldDescription); + + $this->templateRegistry->getTemplate('base_list_field')->shouldHaveBeenCalled(); + } + + /** + * @group legacy + * @expectedDeprecation The Sonata\AdminBundle\Admin\AbstractAdmin::getTemplate method is deprecated (since sonata-project/admin-bundle 3.34, will be dropped in 4.0. Use TemplateRegistry services instead). + */ + public function testRenderWithDebug(): void + { + $this->fieldDescription + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/base_list_field.html.twig'); + + $this->fieldDescription + ->method('getFieldName') + ->willReturn('fd_name'); + + $this->fieldDescription + ->method('getValue') + ->willReturn('foo'); + + $parameters = [ + 'admin' => $this->admin, + 'value' => 'foo', + 'field_description' => $this->fieldDescription, + 'object' => $this->object, + ]; + + $this->environment->enableDebug(); + + $this->assertSame( + $this->removeExtraWhitespace( + <<<'EOT' + + foo + +EOT + ), + $this->removeExtraWhitespace( + $this->twigExtension->renderListElement($this->environment, $this->object, $this->fieldDescription, $parameters) + ) + ); + } + + /** + * @dataProvider getRenderViewElementTests + */ + public function testRenderViewElement(string $expected, string $type, $value, array $options): void + { + $this->admin + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/base_show_field.html.twig'); + + $this->fieldDescription + ->method('getValue') + ->willReturn($value); + + $this->fieldDescription + ->method('getType') + ->willReturn($type); + + $this->fieldDescription + ->method('getOptions') + ->willReturn($options); + + $this->fieldDescription + ->method('getTemplate') + ->willReturnCallback(static function () use ($type): ?string { + switch ($type) { + case 'boolean': + return '@SonataAdmin/CRUD/show_boolean.html.twig'; + case 'datetime': + return '@SonataAdmin/CRUD/show_datetime.html.twig'; + case 'date': + return '@SonataAdmin/CRUD/show_date.html.twig'; + case 'time': + return '@SonataAdmin/CRUD/show_time.html.twig'; + case 'currency': + return '@SonataAdmin/CRUD/show_currency.html.twig'; + case 'percent': + return '@SonataAdmin/CRUD/show_percent.html.twig'; + case 'email': + return '@SonataAdmin/CRUD/show_email.html.twig'; + case 'choice': + return '@SonataAdmin/CRUD/show_choice.html.twig'; + case 'array': + return '@SonataAdmin/CRUD/show_array.html.twig'; + case 'trans': + return '@SonataAdmin/CRUD/show_trans.html.twig'; + case 'url': + return '@SonataAdmin/CRUD/show_url.html.twig'; + case 'html': + return '@SonataAdmin/CRUD/show_html.html.twig'; + default: + return null; + } + }); + + $this->assertSame( + $this->removeExtraWhitespace($expected), + $this->removeExtraWhitespace( + $this->twigExtension->renderViewElement( + $this->environment, + $this->fieldDescription, + $this->object + ) + ) + ); + } + + /** + * @group legacy + * @assertDeprecation Accessing a non existing value is deprecated since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0. + * + * @dataProvider getRenderViewElementWithNoValueTests + */ + public function testRenderViewElementWithNoValue(string $expected, string $type, array $options): void + { + $this->admin + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/base_show_field.html.twig'); + + $this->fieldDescription + ->method('getValue') + ->willThrowException(new NoValueException()); + + $this->fieldDescription + ->method('getType') + ->willReturn($type); + + $this->fieldDescription + ->method('getOptions') + ->willReturn($options); + + $this->fieldDescription + ->method('getTemplate') + ->willReturnCallback(static function () use ($type): ?string { + switch ($type) { + case 'boolean': + return '@SonataAdmin/CRUD/show_boolean.html.twig'; + case 'datetime': + return '@SonataAdmin/CRUD/show_datetime.html.twig'; + case 'date': + return '@SonataAdmin/CRUD/show_date.html.twig'; + case 'time': + return '@SonataAdmin/CRUD/show_time.html.twig'; + case 'currency': + return '@SonataAdmin/CRUD/show_currency.html.twig'; + case 'percent': + return '@SonataAdmin/CRUD/show_percent.html.twig'; + case 'email': + return '@SonataAdmin/CRUD/show_email.html.twig'; + case 'choice': + return '@SonataAdmin/CRUD/show_choice.html.twig'; + case 'array': + return '@SonataAdmin/CRUD/show_array.html.twig'; + case 'trans': + return '@SonataAdmin/CRUD/show_trans.html.twig'; + case 'url': + return '@SonataAdmin/CRUD/show_url.html.twig'; + case 'html': + return '@SonataAdmin/CRUD/show_html.html.twig'; + default: + return null; + } + }); + + $this->assertSame( + $this->removeExtraWhitespace($expected), + $this->removeExtraWhitespace( + $this->twigExtension->renderViewElement( + $this->environment, + $this->fieldDescription, + $this->object + ) + ) + ); + } + + public function getRenderViewElementWithNoValueTests(): iterable + { + return [ + // NoValueException + ['Data ', 'string', ['safe' => false]], + ['Data ', 'text', ['safe' => false]], + ['Data ', 'textarea', ['safe' => false]], + ['Data  ', 'datetime', []], + [ + 'Data  ', + 'datetime', + ['format' => 'd.m.Y H:i:s'], + ], + ['Data  ', 'date', []], + ['Data  ', 'date', ['format' => 'd.m.Y']], + ['Data  ', 'time', []], + ['Data ', 'number', ['safe' => false]], + ['Data ', 'integer', ['safe' => false]], + ['Data  ', 'percent', []], + ['Data  ', 'currency', ['currency' => 'EUR']], + ['Data  ', 'currency', ['currency' => 'GBP']], + ['Data ', 'array', ['safe' => false]], + [ + 'Data no', + 'boolean', + [], + ], + [ + 'Data ', + 'trans', + ['safe' => false, 'catalogue' => 'SonataAdminBundle'], + ], + [ + 'Data ', + 'choice', + ['safe' => false, 'choices' => []], + ], + [ + 'Data ', + 'choice', + ['safe' => false, 'choices' => [], 'multiple' => true], + ], + ['Data  ', 'url', []], + [ + 'Data  ', + 'url', + ['url' => 'http://example.com'], + ], + [ + 'Data  ', + 'url', + ['route' => ['name' => 'sonata_admin_foo']], + ], + ]; + } + + /** + * @dataProvider getRenderViewElementCompareTests + */ + public function testRenderViewElementCompare(string $expected, string $type, $value, array $options, ?string $objectName = null): void + { + $this->admin + ->method('getTemplate') + ->willReturn('@SonataAdmin/CRUD/base_show_compare.html.twig'); + + $this->fieldDescription + ->method('getValue') + ->willReturn($value); + + $this->fieldDescription + ->method('getType') + ->willReturn($type); + + $this->fieldDescription + ->method('getOptions') + ->willReturn($options); + + $this->fieldDescription + ->method('getTemplate') + ->willReturnCallback(static function () use ($type, $options): ?string { + if (isset($options['template'])) { + return $options['template']; + } + + switch ($type) { + case 'boolean': + return '@SonataAdmin/CRUD/show_boolean.html.twig'; + case 'datetime': + return '@SonataAdmin/CRUD/show_datetime.html.twig'; + case 'date': + return '@SonataAdmin/CRUD/show_date.html.twig'; + case 'time': + return '@SonataAdmin/CRUD/show_time.html.twig'; + case 'currency': + return '@SonataAdmin/CRUD/show_currency.html.twig'; + case 'percent': + return '@SonataAdmin/CRUD/show_percent.html.twig'; + case 'email': + return '@SonataAdmin/CRUD/show_email.html.twig'; + case 'choice': + return '@SonataAdmin/CRUD/show_choice.html.twig'; + case 'array': + return '@SonataAdmin/CRUD/show_array.html.twig'; + case 'trans': + return '@SonataAdmin/CRUD/show_trans.html.twig'; + case 'url': + return '@SonataAdmin/CRUD/show_url.html.twig'; + case 'html': + return '@SonataAdmin/CRUD/show_html.html.twig'; + default: + return null; + } + }); + + $this->object->name = 'SonataAdmin'; + + $comparedObject = clone $this->object; + + if (null !== $objectName) { + $comparedObject->name = $objectName; + } + + $this->assertSame( + $this->removeExtraWhitespace($expected), + $this->removeExtraWhitespace( + $this->twigExtension->renderViewElementCompare( + $this->environment, + $this->fieldDescription, + $this->object, + $comparedObject + ) + ) + ); + } + + public function testRenderRelationElementNoObject(): void + { + $this->assertSame('foo', $this->twigExtension->renderRelationElement('foo', $this->fieldDescription)); + } + + public function testRenderRelationElementToString(): void + { + $this->fieldDescription->expects($this->exactly(2)) + ->method('getOption') + ->willReturnCallback(static function ($value, $default = null) { + if ('associated_property' === $value) { + return $default; + } + }); + + $element = new FooToString(); + $this->assertSame('salut', $this->twigExtension->renderRelationElement($element, $this->fieldDescription)); + } + + /** + * @group legacy + */ + public function testDeprecatedRelationElementToString(): void + { + $this->fieldDescription->expects($this->exactly(2)) + ->method('getOption') + ->willReturnCallback(static function ($value, $default = null) { + if ('associated_tostring' === $value) { + return '__toString'; + } + }); + + $element = new FooToString(); + $this->assertSame( + 'salut', + $this->twigExtension->renderRelationElement($element, $this->fieldDescription) + ); + } + + /** + * @group legacy + */ + public function testRenderRelationElementCustomToString(): void + { + $this->fieldDescription->expects($this->exactly(2)) + ->method('getOption') + ->willReturnCallback(static function ($value, $default = null) { + if ('associated_property' === $value) { + return $default; + } + + if ('associated_tostring' === $value) { + return 'customToString'; + } + }); + + $element = $this->getMockBuilder(\stdClass::class) + ->setMethods(['customToString']) + ->getMock(); + $element + ->method('customToString') + ->willReturn('fooBar'); + + $this->assertSame('fooBar', $this->twigExtension->renderRelationElement($element, $this->fieldDescription)); + } + + /** + * @group legacy + */ + public function testRenderRelationElementMethodNotExist(): void + { + $this->fieldDescription->expects($this->exactly(2)) + ->method('getOption') + + ->willReturnCallback(static function ($value, $default = null) { + if ('associated_tostring' === $value) { + return 'nonExistedMethod'; + } + }); + + $element = new \stdClass(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('You must define an `associated_property` option or create a `stdClass::__toString'); + + $this->twigExtension->renderRelationElement($element, $this->fieldDescription); + } + + public function testRenderRelationElementWithPropertyPath(): void + { + $this->fieldDescription->expects($this->once()) + ->method('getOption') + + ->willReturnCallback(static function ($value, $default = null) { + if ('associated_property' === $value) { + return 'foo'; + } + }); + + $element = new \stdClass(); + $element->foo = 'bar'; + + $this->assertSame('bar', $this->twigExtension->renderRelationElement($element, $this->fieldDescription)); + } + + public function testRenderRelationElementWithClosure(): void + { + $this->fieldDescription->expects($this->once()) + ->method('getOption') + + ->willReturnCallback(static function ($value, $default = null) { + if ('associated_property' === $value) { + return static function ($element): string { + return sprintf('closure %s', $element->foo); + }; + } + }); + + $element = new \stdClass(); + $element->foo = 'bar'; + + $this->assertSame( + 'closure bar', + $this->twigExtension->renderRelationElement($element, $this->fieldDescription) + ); + } + + public function getRenderListElementTests() + { + return [ + [ + ' Example ', + 'string', + 'Example', + [], + ], + [ + ' ', + 'string', + null, + [], + ], + [ + ' Example ', + 'text', + 'Example', + [], + ], + [ + ' ', + 'text', + null, + [], + ], + [ + ' Example ', + 'textarea', + 'Example', + [], + ], + [ + ' ', + 'textarea', + null, + [], + ], + 'datetime field' => [ + ' + + ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + ' + + ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + [ + '   ', + 'datetime', + null, + [], + ], + [ + ' + + ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y H:i:s'], + ], + [ + '   ', + 'datetime', + null, + ['format' => 'd.m.Y H:i:s'], + ], + [ + ' + + ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['format' => 'd.m.Y H:i:s', 'timezone' => 'Asia/Hong_Kong'], + ], + [ + '   ', + 'datetime', + null, + ['format' => 'd.m.Y H:i:s', 'timezone' => 'Asia/Hong_Kong'], + ], + [ + ' + + ', + 'date', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + '   ', + 'date', + null, + [], + ], + [ + ' + + ', + 'date', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y'], + ], + [ + '   ', + 'date', + null, + ['format' => 'd.m.Y'], + ], + [ + ' + + ', + 'time', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + ' + + ', + 'time', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + [ + '   ', + 'time', + null, + [], + ], + [ + ' 10.746135 ', + 'number', 10.746135, + [], + ], + [ + ' ', + 'number', + null, + [], + ], + [ + ' 5678 ', + 'integer', + 5678, + [], + ], + [ + ' ', + 'integer', + null, + [], + ], + [ + ' 1074.6135 % ', + 'percent', + 10.746135, + [], + ], + [ + ' 0 % ', + 'percent', + 0, + [], + ], + [ + '   ', + 'percent', + null, + [], + ], + [ + ' EUR 10.746135 ', + 'currency', + 10.746135, + ['currency' => 'EUR'], + ], + [ + ' EUR 0 ', + 'currency', + 0, + ['currency' => 'EUR'], + ], + [ + ' GBP 51.23456 ', + 'currency', + 51.23456, + ['currency' => 'GBP'], + ], + [ + '   ', + 'currency', + null, + ['currency' => 'GBP'], + ], + [ + '   ', + 'email', + null, + [], + ], + [ + ' admin@admin.com ', + 'email', + 'admin@admin.com', + [], + ], + [ + ' + admin@admin.com ', + 'email', + 'admin@admin.com', + ['as_string' => false], + ], + [ + ' admin@admin.com ', + 'email', + 'admin@admin.com', + ['as_string' => true], + ], + [ + ' + admin@admin.com ', + 'email', + 'admin@admin.com', + ['subject' => 'Main Theme', 'body' => 'Message Body'], + ], + [ + ' + admin@admin.com ', + 'email', + 'admin@admin.com', + ['subject' => 'Main Theme'], + ], + [ + ' + admin@admin.com ', + 'email', + 'admin@admin.com', + ['body' => 'Message Body'], + ], + [ + ' admin@admin.com ', + 'email', + 'admin@admin.com', + ['as_string' => true, 'subject' => 'Main Theme', 'body' => 'Message Body'], + ], + [ + ' admin@admin.com ', + 'email', + 'admin@admin.com', + ['as_string' => true, 'body' => 'Message Body'], + ], + [ + ' admin@admin.com ', + 'email', + 'admin@admin.com', + ['as_string' => true, 'subject' => 'Main Theme'], + ], + [ + ' + [1 => First, 2 => Second] + ', + 'array', + [1 => 'First', 2 => 'Second'], + [], + ], + [ + ' [] ', + 'array', + null, + [], + ], + [ + ' + yes + ', + 'boolean', + true, + ['editable' => false], + ], + [ + ' + no + ', + 'boolean', + false, + ['editable' => false], + ], + [ + ' + no + ', + 'boolean', + null, + ['editable' => false], + ], + [ + <<<'EOT' + + + yes + + +EOT + , + 'boolean', + true, + ['editable' => true], + ], + [ + <<<'EOT' + + + no + +EOT + , + 'boolean', + false, + ['editable' => true], + ], + [ + <<<'EOT' + + + no + +EOT + , + 'boolean', + null, + ['editable' => true], + ], + [ + ' Delete ', + 'trans', + 'action_delete', + ['catalogue' => 'SonataAdminBundle'], + ], + [ + ' ', + 'trans', + null, + ['catalogue' => 'SonataAdminBundle'], + ], + [ + ' Delete ', + 'trans', + 'action_delete', + ['format' => '%s', 'catalogue' => 'SonataAdminBundle'], + ], + [ + ' + action.action_delete + ', + 'trans', + 'action_delete', + ['format' => 'action.%s'], + ], + [ + ' + action.action_delete + ', + 'trans', + 'action_delete', + ['format' => 'action.%s', 'catalogue' => 'SonataAdminBundle'], + ], + [ + ' Status1 ', + 'choice', + 'Status1', + [], + ], + [ + ' Status1 ', + 'choice', + ['Status1'], + ['choices' => [], 'multiple' => true], + ], + [ + ' Alias1 ', + 'choice', + 'Status1', + ['choices' => ['Status1' => 'Alias1', 'Status2' => 'Alias2', 'Status3' => 'Alias3']], + ], + [ + ' ', + 'choice', + null, + ['choices' => ['Status1' => 'Alias1', 'Status2' => 'Alias2', 'Status3' => 'Alias3']], + ], + [ + ' + NoValidKeyInChoices + ', + 'choice', + 'NoValidKeyInChoices', + ['choices' => ['Status1' => 'Alias1', 'Status2' => 'Alias2', 'Status3' => 'Alias3']], + ], + [ + ' Delete ', + 'choice', + 'Foo', + ['catalogue' => 'SonataAdminBundle', 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + ' Alias1, Alias3 ', + 'choice', + ['Status1', 'Status3'], + ['choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], ], + [ + ' Alias1 | Alias3 ', + 'choice', + ['Status1', 'Status3'], + ['choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true, 'delimiter' => ' | '], ], + [ + ' ', + 'choice', + null, + ['choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + ' + NoValidKeyInChoices + ', + 'choice', + ['NoValidKeyInChoices'], + ['choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + ' + NoValidKeyInChoices, Alias2 + ', + 'choice', + ['NoValidKeyInChoices', 'Status2'], + ['choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + ' Delete, Alias3 ', + 'choice', + ['Foo', 'Status3'], + ['catalogue' => 'SonataAdminBundle', 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + ' + <b>Alias1</b>, <b>Alias3</b> + ', + 'choice', + ['Status1', 'Status3'], + ['choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], ], + [ + <<<'EOT' + + + Status1 + + +EOT + , + 'choice', + 'Status1', + ['editable' => true], + ], + [ + <<<'EOT' + + + Alias1 + +EOT + , + 'choice', + 'Status1', + [ + 'editable' => true, + 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], + ], + ], + [ + <<<'EOT' + + + + + +EOT + , + 'choice', + null, + [ + 'editable' => true, + 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], + ], + ], + [ + <<<'EOT' + + + NoValidKeyInChoices + + +EOT + , + 'choice', + 'NoValidKeyInChoices', + [ + 'editable' => true, + 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], + ], + ], + [ + <<<'EOT' + + + Delete + + +EOT + , + 'choice', + 'Foo', + [ + 'editable' => true, + 'catalogue' => 'SonataAdminBundle', + 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], + ], + ], + [ + '   ', + 'url', + null, + [], + ], + [ + '   ', + 'url', + null, + ['url' => 'http://example.com'], + ], + [ + '   ', + 'url', + null, + ['route' => ['name' => 'sonata_admin_foo']], + ], + [ + ' + http://example.com + ', + 'url', + 'http://example.com', + [], + ], + [ + ' + https://example.com + ', + 'url', + 'https://example.com', + [], + ], + [ + ' + https://example.com + ', + 'url', + 'https://example.com', + ['attributes' => ['target' => '_blank']], + ], + [ + ' + https://example.com + ', + 'url', + 'https://example.com', + ['attributes' => ['target' => '_blank', 'class' => 'fooLink']], + ], + [ + ' + example.com + ', + 'url', + 'http://example.com', + ['hide_protocol' => true], + ], + [ + ' + example.com + ', + 'url', + 'https://example.com', + ['hide_protocol' => true], + ], + [ + ' + http://example.com + ', + 'url', + 'http://example.com', + ['hide_protocol' => false], + ], + [ + ' + https://example.com + ', + 'url', + 'https://example.com', + ['hide_protocol' => false], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + ['url' => 'http://example.com'], + ], + [ + ' + <b>Foo</b> + ', + 'url', + 'Foo', + ['url' => 'http://example.com'], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + ['route' => ['name' => 'sonata_admin_foo']], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + ['route' => ['name' => 'sonata_admin_foo', 'absolute' => true]], + ], + [ + ' + foo/bar?a=b&c=123456789 + ', + 'url', + 'http://foo/bar?a=b&c=123456789', + ['route' => ['name' => 'sonata_admin_foo'], + 'hide_protocol' => true, ], + ], + [ + ' + foo/bar?a=b&c=123456789 + ', + 'url', + 'http://foo/bar?a=b&c=123456789', + [ + 'route' => ['name' => 'sonata_admin_foo', 'absolute' => true], + 'hide_protocol' => true, + ], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + [ + 'route' => ['name' => 'sonata_admin_foo_param', + 'parameters' => ['param1' => 'abcd', 'param2' => 'efgh', 'param3' => 'ijkl'], ], + ], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + [ + 'route' => ['name' => 'sonata_admin_foo_param', + 'absolute' => true, + 'parameters' => ['param1' => 'abcd', 'param2' => 'efgh', 'param3' => 'ijkl'], ], + ], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + [ + 'route' => ['name' => 'sonata_admin_foo_object', + 'parameters' => ['param1' => 'abcd', 'param2' => 'efgh', 'param3' => 'ijkl'], + 'identifier_parameter_name' => 'barId', ], + ], + ], + [ + ' + Foo + ', + 'url', + 'Foo', + [ + 'route' => ['name' => 'sonata_admin_foo_object', + 'absolute' => true, + 'parameters' => ['param1' => 'abcd', 'param2' => 'efgh', 'param3' => 'ijkl'], + 'identifier_parameter_name' => 'barId', ], + ], + ], + [ + ' +

Creating a Template for the Field and form

+ ', + 'html', + '

Creating a Template for the Field and form

', + [], + ], + [ + ' + Creating a Template for the Field and form + ', + 'html', + '

Creating a Template for the Field and form

', + ['strip' => true], + ], + [ + ' + Creating a Template for the... + ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => true], + ], + [ + ' Creatin... ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => ['length' => 10]], + ], + [ + ' + Creating a Template for the Field... + ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => ['cut' => false]], + ], + [ + ' + Creating a Template for t etc. + ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => ['ellipsis' => ' etc.']], + ], + [ + ' + Creating a Template[...] + ', + 'html', + '

Creating a Template for the Field and form

', + [ + 'truncate' => [ + 'length' => 20, + 'cut' => false, + 'ellipsis' => '[...]', + ], + ], + ], + + [ + <<<'EOT' + +
A very long string
+ +EOT + , + 'text', + 'A very long string', + [ + 'collapse' => true, + ], + ], + [ + <<<'EOT' + +
A very long string
+ +EOT + , + 'text', + 'A very long string', + [ + 'collapse' => [ + 'height' => 10, + 'more' => 'More', + 'less' => 'Less', + ], + ], + ], + [ + <<<'EOT' + + + Delete, Alias2 + + +EOT + , + 'choice', + [ + 'Status1', + 'Status2', + ], + [ + 'editable' => true, + 'multiple' => true, + 'catalogue' => 'SonataAdminBundle', + 'choices' => [ + 'Status1' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], + ], + ], + ]; + } + + public function getDeprecatedRenderListElementTests() + { + return [ + [ + ' Example ', + 'Example', + [], + ], + [ + ' ', + null, + [], + ], + ]; + } + + public function getRenderViewElementTests() + { + return [ + ['Data Example', 'string', 'Example', ['safe' => false]], + ['Data Example', 'text', 'Example', ['safe' => false]], + ['Data Example', 'textarea', 'Example', ['safe' => false]], + [ + 'Data ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), [], + ], + [ + 'Data ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y H:i:s'], + ], + [ + 'Data ', + 'datetime', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + [ + 'Data ', + 'date', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + 'Data ', + 'date', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y'], + ], + [ + 'Data ', + 'time', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + 'Data ', + 'time', + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + ['Data 10.746135', 'number', 10.746135, ['safe' => false]], + ['Data 5678', 'integer', 5678, ['safe' => false]], + ['Data 1074.6135 %', 'percent', 10.746135, []], + ['Data 0 %', 'percent', 0, []], + ['Data EUR 10.746135', 'currency', 10.746135, ['currency' => 'EUR']], + ['Data GBP 51.23456', 'currency', 51.23456, ['currency' => 'GBP']], + ['Data EUR 0', 'currency', 0, ['currency' => 'EUR']], + [ + 'Data ', + 'array', + [1 => 'First', 2 => 'Second'], + ['safe' => false], + ], + [ + 'Data [1 => First, 2 => Second] ', + 'array', + [1 => 'First', 2 => 'Second'], + ['safe' => false, 'inline' => true], + ], + [ + 'Data yes', + 'boolean', + true, + [], + ], + [ + 'Data yes', + 'boolean', + true, + ['inverse' => true], + ], + ['Data no', 'boolean', false, []], + [ + 'Data no', + 'boolean', + false, + ['inverse' => true], + ], + [ + 'Data Delete ', + 'trans', + 'action_delete', + ['safe' => false, 'catalogue' => 'SonataAdminBundle'], + ], + [ + 'Data Delete ', + 'trans', + 'delete', + ['safe' => false, 'catalogue' => 'SonataAdminBundle', 'format' => 'action_%s'], + ], + ['Data Status1', 'choice', 'Status1', ['safe' => false]], + [ + 'Data Alias1', + 'choice', + 'Status1', + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + 'Data NoValidKeyInChoices', + 'choice', + 'NoValidKeyInChoices', + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + 'Data Delete', + 'choice', + 'Foo', + ['safe' => false, 'catalogue' => 'SonataAdminBundle', 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + 'Data NoValidKeyInChoices', + 'choice', + ['NoValidKeyInChoices'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data NoValidKeyInChoices, Alias2', + 'choice', + ['NoValidKeyInChoices', 'Status2'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data Alias1, Alias3', + 'choice', + ['Status1', 'Status3'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data Alias1 | Alias3', + 'choice', + ['Status1', 'Status3'], ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true, 'delimiter' => ' | '], + ], + [ + 'Data Delete, Alias3', + 'choice', + ['Foo', 'Status3'], + ['safe' => false, 'catalogue' => 'SonataAdminBundle', 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data Alias1, Alias3', + 'choice', + ['Status1', 'Status3'], + ['safe' => true, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data <b>Alias1</b>, <b>Alias3</b>', + 'choice', + ['Status1', 'Status3'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data http://example.com', + 'url', + 'http://example.com', + ['safe' => false], + ], + [ + 'Data http://example.com', + 'url', + 'http://example.com', + ['safe' => false, 'attributes' => ['target' => '_blank']], + ], + [ + 'Data http://example.com', + 'url', + 'http://example.com', + ['safe' => false, 'attributes' => ['target' => '_blank', 'class' => 'fooLink']], + ], + [ + 'Data https://example.com', + 'url', + 'https://example.com', + ['safe' => false], + ], + [ + 'Data example.com', + 'url', + 'http://example.com', + ['safe' => false, 'hide_protocol' => true], + ], + [ + 'Data example.com', + 'url', + 'https://example.com', + ['safe' => false, 'hide_protocol' => true], + ], + [ + 'Data http://example.com', + 'url', + 'http://example.com', + ['safe' => false, 'hide_protocol' => false], + ], + [ + 'Data https://example.com', + 'url', + 'https://example.com', + ['safe' => false, + 'hide_protocol' => false, ], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'url' => 'http://example.com'], + ], + [ + 'Data <b>Foo</b>', + 'url', + 'Foo', + ['safe' => false, 'url' => 'http://example.com'], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => true, 'url' => 'http://example.com'], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'route' => ['name' => 'sonata_admin_foo']], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo', + 'absolute' => true, + ]], + ], + [ + 'Data foo/bar?a=b&c=123456789', + 'url', + 'http://foo/bar?a=b&c=123456789', + [ + 'safe' => false, + 'route' => ['name' => 'sonata_admin_foo'], + 'hide_protocol' => true, + ], + ], + [ + 'Data foo/bar?a=b&c=123456789', + 'url', + 'http://foo/bar?a=b&c=123456789', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo', + 'absolute' => true, + ], 'hide_protocol' => true], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_param', + 'parameters' => ['param1' => 'abcd', 'param2' => 'efgh', 'param3' => 'ijkl'], + ]], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_param', + 'absolute' => true, + 'parameters' => [ + 'param1' => 'abcd', + 'param2' => 'efgh', + 'param3' => 'ijkl', + ], + ]], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_object', + 'parameters' => [ + 'param1' => 'abcd', + 'param2' => 'efgh', + 'param3' => 'ijkl', + ], + 'identifier_parameter_name' => 'barId', + ]], + ], + [ + 'Data Foo', + 'url', + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_object', + 'absolute' => true, + 'parameters' => [ + 'param1' => 'abcd', + 'param2' => 'efgh', + 'param3' => 'ijkl', + ], + 'identifier_parameter_name' => 'barId', + ]], + ], + [ + 'Data  ', + 'email', + null, + [], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + [], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['subject' => 'Main Theme', 'body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['subject' => 'Main Theme'], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['as_string' => true, 'subject' => 'Main Theme', 'body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['as_string' => true, 'subject' => 'Main Theme'], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['as_string' => true, 'body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['as_string' => false], + ], + [ + 'Data admin@admin.com', + 'email', + 'admin@admin.com', + ['as_string' => true], + ], + [ + 'Data

Creating a Template for the Field and form

', + 'html', + '

Creating a Template for the Field and form

', + [], + ], + [ + 'Data Creating a Template for the Field and form ', + 'html', + '

Creating a Template for the Field and form

', + ['strip' => true], + ], + [ + 'Data Creating a Template for the... ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => true], + ], + [ + 'Data Creatin... ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => ['length' => 10]], + ], + [ + 'Data Creating a Template for the Field... ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => ['cut' => false]], + ], + [ + 'Data Creating a Template for t etc. ', + 'html', + '

Creating a Template for the Field and form

', + ['truncate' => ['ellipsis' => ' etc.']], + ], + [ + 'Data Creating a Template[...] ', + 'html', + '

Creating a Template for the Field and form

', + [ + 'truncate' => [ + 'length' => 20, + 'cut' => false, + 'ellipsis' => '[...]', + ], + ], + ], + [ + <<<'EOT' +Data
+ A very long string +
+EOT + , + 'text', + ' A very long string ', + [ + 'collapse' => true, + 'safe' => false, + ], + ], + [ + <<<'EOT' +Data
+ A very long string +
+EOT + , + 'text', + ' A very long string ', + [ + 'collapse' => [ + 'height' => 10, + 'more' => 'More', + 'less' => 'Less', + ], + 'safe' => false, + ], + ], + ]; + } + + public function getRenderViewElementCompareTests(): iterable + { + return [ + ['Data ExampleExample', 'string', 'Example', ['safe' => false]], + ['Data ExampleExample', 'text', 'Example', ['safe' => false]], + ['Data ExampleExample', 'textarea', 'Example', ['safe' => false]], + ['Data SonataAdmin
ExampleSonataAdmin
Example', 'virtual_field', 'Example', ['template' => 'custom_show_field.html.twig', 'safe' => false, 'SonataAdmin']], + ['Data SonataAdmin
Examplesonata-project/admin-bundle
Example', 'virtual_field', 'Example', ['template' => 'custom_show_field.html.twig', 'safe' => false], 'sonata-project/admin-bundle'], + [ + 'Data ' + .'', + 'datetime', + new \DateTime('2020-05-27 10:11:12', new \DateTimeZone('Europe/London')), [], + ], + [ + 'Data ' + .'', + 'datetime', + new \DateTime('2020-05-27 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y H:i:s'], + ], + [ + 'Data ' + .'', + 'datetime', + new \DateTime('2020-05-27 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + [ + 'Data ' + .'', + 'date', + new \DateTime('2020-05-27 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + ]; + } + + /** + * This method generates url part for Twig layout. + */ + private function buildTwigLikeUrl(array $url): string + { + return htmlspecialchars(http_build_query($url, '', '&', PHP_QUERY_RFC3986)); + } + + private function removeExtraWhitespace(string $string): string + { + return trim(preg_replace( + '/\s+/', + ' ', + $string + )); + } +} diff --git a/tests/Twig/Extension/SonataAdminExtensionTest.php b/tests/Twig/Extension/SonataAdminExtensionTest.php index eb652b48b4d..d0e24c1d388 100644 --- a/tests/Twig/Extension/SonataAdminExtensionTest.php +++ b/tests/Twig/Extension/SonataAdminExtensionTest.php @@ -2551,7 +2551,7 @@ public function testOutput(): void $this->environment->enableDebug(); - $this->expectDeprecation('The Sonata\AdminBundle\Twig\Extension\SonataAdminExtension::render method is deprecated in favor of RenderElementExtension::render since version 3.x and will be removed in 4.0.'); + $this->expectDeprecation('The Sonata\AdminBundle\Twig\Extension\SonataAdminExtension::output method is deprecated since version 3.33 and will be removed in 4.0.'); $this->assertSame( $this->removeExtraWhitespace(