From 502d0ec1eb6b7f20cd5d3b847c175f66e8247c55 Mon Sep 17 00:00:00 2001 From: Jorrit Schippers Date: Tue, 14 Jul 2020 12:19:42 +0200 Subject: [PATCH] Add support for displaying additional values in list views --- docs/reference/action_list.rst | 54 +++++++++++++ src/Twig/Extension/SonataAdminExtension.php | 65 ++++++++++++++- .../Extension/SonataAdminExtensionTest.php | 79 ++++++++++++++++++- 3 files changed, 193 insertions(+), 5 deletions(-) diff --git a/docs/reference/action_list.rst b/docs/reference/action_list.rst index ea62dba29ca..7f0ae14b134 100644 --- a/docs/reference/action_list.rst +++ b/docs/reference/action_list.rst @@ -578,5 +578,59 @@ Checkbox range selection You can check / uncheck a range of checkboxes by clicking a first one, then a second one with shift + click. +Displaying a non-model field +---------------------------- + +.. versionadded:: 3.x + + Support for displaying fields not part of the model class was introduced in version 3.x. + +The list view can also display fields that are not part of the model class. + +In some situations you can add a new getter to your model class to calculate +a field based on the other fields of your model:: + + // src/Entity/User.php + + public function getFullName(): string + { + return $this->getGivenName().' '.$this->getFamilyName(); + } + + // src/Admin/UserAdmin.php + + protected function configureListFields(ListMapper $listMapper) + { + $listMapper->addIdentifier('fullName'); + } + +In situations where the data are not available in the model or it is more performant +to have the database calculate the value you can override the ``configureQuery()`` Admin +class method to add fields to the result set. +In ``configureListFields()`` these fields can be added using the alias given +in the query. + +In the following example the number of comments for a post is added to the +query and displayed:: + + // src/Admin/PostAdmin.php + + protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface + { + $query = parent::configureQuery($query); + + $query + ->leftJoin('n.Comments', 'c') + ->addSelect('COUNT(c.id) numberofcomments') + ->addGroupBy('n'); + + return $query; + } + + protected function configureListFields(ListMapper $listMapper) + { + $listMapper->addIdentifier('numberofcomments'); + } + .. _`SonataDoctrineORMAdminBundle Documentation`: https://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/list_field_definition.html .. _`here`: https://github.com/sonata-project/SonataCoreBundle/tree/3.x/src/Form/Type diff --git a/src/Twig/Extension/SonataAdminExtension.php b/src/Twig/Extension/SonataAdminExtension.php index 5cf65daf712..b63d058cd0e 100644 --- a/src/Twig/Extension/SonataAdminExtension.php +++ b/src/Twig/Extension/SonataAdminExtension.php @@ -184,14 +184,14 @@ public function getName() /** * render a list element from the FieldDescription. * - * @param object $object - * @param array $params + * @param object|array $listElement + * @param array $params * * @return string */ public function renderListElement( Environment $environment, - $object, + $listElement, FieldDescriptionInterface $fieldDescription, $params = [] ) { @@ -203,10 +203,12 @@ public function renderListElement( $environment ); + [$object, $value] = $this->getObjectAndValueFromListElement($listElement, $fieldDescription); + return $this->render($fieldDescription, $template, array_merge($params, [ 'admin' => $fieldDescription->getAdmin(), 'object' => $object, - 'value' => $this->getValueFromFieldDescription($object, $fieldDescription), + 'value' => $value, 'field_description' => $fieldDescription, ]), $environment); } @@ -238,6 +240,10 @@ public function output( * * @throws \RuntimeException * + * NEXT_MAJOR: Remove this method + * + * @deprecated This method is deprecated with no replacement since sonata-project/admin-bundle 3.x and will be removed in 4.0. + * * @return mixed */ public function getValueFromFieldDescription( @@ -245,6 +251,12 @@ public function getValueFromFieldDescription( FieldDescriptionInterface $fieldDescription, array $params = [] ) { + @trigger_error(sprintf( + 'The %s method is deprecated since sonata-project/admin-bundle 3.x and will be removed in version 4.0.' + .' There is no replacement.', + __METHOD__ + ), E_USER_DEPRECATED); + if (isset($params['loop']) && $object instanceof \ArrayAccess) { throw new \RuntimeException('remove the loop requirement'); } @@ -695,4 +707,49 @@ private function getTemplateRegistry(string $adminCode): TemplateRegistryInterfa throw new ServiceNotFoundException($serviceId); } + + /** + * Extracts the object and requested value from the $listElement. + * + * @param object|array $listElement + * + * @throws \TypeError when $listElement is not an object or an array with an object on offset 0 + * + * @return array An array containing object and value + */ + private function getObjectAndValueFromListElement( + $listElement, + FieldDescriptionInterface $fieldDescription + ): array { + if (\is_object($listElement)) { + $object = $listElement; + } elseif (\is_array($listElement)) { + if (!isset($listElement[0]) || !\is_object($listElement[0])) { + throw new \TypeError(sprintf('If argument 1 passed to %s() is an array it must contain an object offet 0', __METHOD__)); + } + + $object = $listElement[0]; + } else { + throw new \TypeError(sprintf('Argument 1 passed to %s() must be an object or an array, %s given.', __METHOD__, \gettype($listElement))); + } + + if (\is_array($listElement) && \array_key_exists($fieldDescription->getName(), $listElement)) { + $value = $listElement[$fieldDescription->getName()]; + } else { + try { + $value = $fieldDescription->getValue($object); + } catch (NoValueException $e) { + // NEXT_MAJOR: throw the NoValueException. + @trigger_error( + 'Accessing a non existing value is deprecated' + .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.', + E_USER_DEPRECATED + ); + + $value = null; + } + } + + return [$object, $value]; + } } diff --git a/tests/Twig/Extension/SonataAdminExtensionTest.php b/tests/Twig/Extension/SonataAdminExtensionTest.php index 89ab2f20b1c..ce993ee2cf0 100644 --- a/tests/Twig/Extension/SonataAdminExtensionTest.php +++ b/tests/Twig/Extension/SonataAdminExtensionTest.php @@ -386,6 +386,68 @@ public function testRenderListElement(string $expected, string $type, $value, ar ); } + /** + * 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->getTemplate('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->getTemplate('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 @@ -2261,6 +2323,11 @@ public function getDeprecatedTextExtensionItems(): iterable ]; } + /** + * NEXT_MAJOR: Remove this test. + * + * @group legacy + */ public function testGetValueFromFieldDescription(): void { $object = new \stdClass(); @@ -2273,6 +2340,11 @@ public function testGetValueFromFieldDescription(): void $this->assertSame('test123', $this->twigExtension->getValueFromFieldDescription($object, $fieldDescription)); } + /** + * NEXT_MAJOR: Remove this test. + * + * @group legacy + */ public function testGetValueFromFieldDescriptionWithRemoveLoopException(): void { $object = $this->createMock(\ArrayAccess::class); @@ -2288,7 +2360,7 @@ public function testGetValueFromFieldDescriptionWithRemoveLoopException(): void } /** - * NEXT_MAJOR: Change this test to expect a NoValueException instead. + * NEXT_MAJOR: Remove this test. * * @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. @@ -2311,6 +2383,11 @@ public function testGetValueFromFieldDescriptionWithNoValueException(): void $this->assertNull($this->twigExtension->getValueFromFieldDescription($object, $fieldDescription)); } + /** + * NEXT_MAJOR: Remove this test. + * + * @group legacy + */ public function testGetValueFromFieldDescriptionWithNoValueExceptionNewAdminInstance(): void { $object = new \stdClass();