From 49aa482942bb7c8d08f6d7cd5561fe197fea37e0 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 9 Feb 2021 13:11:53 +0100 Subject: [PATCH] Add accessor property instead of code (#6837) * Add accessor property instead of code * Prefer property path instead of method name * Update doc --- UPGRADE-3.x.md | 4 ++ docs/reference/action_list.rst | 10 +++++ src/Admin/BaseFieldDescription.php | 54 ++++++++++++------------ tests/Admin/BaseFieldDescriptionTest.php | 25 +++++++++++ 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/UPGRADE-3.x.md b/UPGRADE-3.x.md index b1750d450d..9019163f04 100644 --- a/UPGRADE-3.x.md +++ b/UPGRADE-3.x.md @@ -34,6 +34,10 @@ final class MyAdmin extends AbstractAdmin } ``` +### Deprecated the `Sonata\AdminBundle\AdminFieldDescription` `'code'` option. + +Use the `accessor` option instead. + ### Deprecated `Sonata\AdminBundle\Admin\AbstractAdmin::formOptions` property. This property has been replaced by the new method `Sonata\AdminBundle\Admin\AbstractAdmin::configureFormOptions()` diff --git a/docs/reference/action_list.rst b/docs/reference/action_list.rst index 674b6ce595..f658462be0 100644 --- a/docs/reference/action_list.rst +++ b/docs/reference/action_list.rst @@ -99,6 +99,16 @@ Here is an example:: // specific properties of a relation to the entity ->add('image.name') + // you may also use a custom accessor + ->add('description1', null, [ + 'accessor' => 'description' + ]) + ->add('description2', null, [ + 'accessor' => function ($subject) { + return $this->customService->formatDescription($subject); + } + ]) + // You may also specify the actions you want to be displayed in the list ->add('_action', null, [ 'actions' => [ diff --git a/src/Admin/BaseFieldDescription.php b/src/Admin/BaseFieldDescription.php index 3def42383e..fd05832c58 100644 --- a/src/Admin/BaseFieldDescription.php +++ b/src/Admin/BaseFieldDescription.php @@ -18,6 +18,7 @@ use Sonata\AdminBundle\Exception\NoValueException; use Symfony\Component\PropertyAccess\Exception\ExceptionInterface; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyPathInterface; /** * A FieldDescription hold the information about a field. A typical @@ -36,7 +37,7 @@ * - name (o) : the name used (label in the form, title in the list) * - link_parameters (o) : add link parameter to the related Admin class when * the Admin.generateUrl is called - * - code : the method name to retrieve the related value + * - accessor : the method or the property path to retrieve the related value * - associated_tostring : (deprecated, use associated_property option) * the method to retrieve the "string" representation * of the collection element. @@ -418,40 +419,37 @@ public function getFieldValue($object, $fieldName) return $this->getFieldValue($child, substr($fieldName, $dotPos + 1)); } - // prefer method name given in the code option + // NEXT_MAJOR: Remove this code. if ($this->getOption('code')) { - $getter = $this->getOption('code'); - - if (!method_exists($object, $getter)) { - @trigger_error( - 'Passing a non-existing method in the "code" option is deprecated' - .' since sonata-project/admin-bundle 3.x and will throw an exception in 4.0.', - \E_USER_DEPRECATED - ); + @trigger_error( + 'The "code" option is deprecated since sonata-project/admin-bundle 3.x.' + .' Use the "accessor" code instead', + \E_USER_DEPRECATED + ); - // NEXT_MAJOR: Remove the deprecation and uncomment the next line. -// throw new \LogicException('The method "%s"() does not exist.', $getter); - } elseif (!\is_callable([$object, $getter])) { + $getter = $this->getOption('code'); + if ($this->getOption('parameters')) { @trigger_error( - 'Passing a non-callable method in the "code" option is deprecated' - .' since sonata-project/admin-bundle 3.x and will throw an exception in 4.0.', + 'The option "parameters" is deprecated since sonata-project/admin-bundle 3.x and will be removed in 4.0.', \E_USER_DEPRECATED ); - // NEXT_MAJOR: Remove the deprecation and uncomment the next line. -// throw new \LogicException('The method "%s"() does not have public access.', $getter); - } else { - if ($this->getOption('parameters')) { - @trigger_error( - 'The option "parameters" is deprecated since sonata-project/admin-bundle 3.x and will be removed in 4.0.', - \E_USER_DEPRECATED - ); + return $object->{$getter}(...$this->getOption('parameters')); + } - return $object->{$getter}(...$this->getOption('parameters')); - } + return $object->{$getter}(); + } - return $object->{$getter}(); - } + // prefer the method or the property path given in the code option + $accessor = $this->getOption('accessor', $fieldName); + if (\is_callable($accessor)) { + return $accessor($object); + } elseif (!\is_string($accessor) && !$accessor instanceof PropertyPathInterface) { + throw new \TypeError(sprintf( + 'The option "accessor" must be a string, a callable or a %s, %s given.', + PropertyPathInterface::class, + \is_object($accessor) ? 'instance of '.\get_class($accessor) : \gettype($accessor) + )); } // NEXT_MAJOR: Remove the condition code and the else part @@ -461,7 +459,7 @@ public function getFieldValue($object, $fieldName) ->getPropertyAccessor(); try { - return $propertyAccesor->getValue($object, $fieldName); + return $propertyAccesor->getValue($object, $accessor); } catch (ExceptionInterface $exception) { throw new NoValueException( sprintf('Cannot access property "%s" in class "%s".', $this->getName(), \get_class($object)), diff --git a/tests/Admin/BaseFieldDescriptionTest.php b/tests/Admin/BaseFieldDescriptionTest.php index d131f24b31..2dfcfa5b90 100644 --- a/tests/Admin/BaseFieldDescriptionTest.php +++ b/tests/Admin/BaseFieldDescriptionTest.php @@ -186,6 +186,31 @@ public function testGetFieldValueWithNullObject(): void $this->assertNull($description->getFieldValue(null, 'fake')); } + public function testGetFieldValueWithAccessor(): void + { + $description = new FieldDescription('name', ['accessor' => 'foo']); + $mock = $this->getMockBuilder(\stdClass::class)->addMethods(['getFoo'])->getMock(); + $mock->expects($this->once())->method('getFoo')->willReturn(42); + $this->assertSame(42, $description->getFieldValue($mock, 'fake')); + } + + public function testGetFieldValueWithCallableAccessor(): void + { + $description = new FieldDescription('name', [ + 'accessor' => static function (object $object): int { + return $object->getFoo(); + }, + ]); + $mock = $this->getMockBuilder(\stdClass::class)->addMethods(['getFoo'])->getMock(); + $mock->expects($this->once())->method('getFoo')->willReturn(42); + $this->assertSame(42, $description->getFieldValue($mock, 'fake')); + } + + /** + * NEXT_MAJOR: Remove this test. + * + * @group legacy + */ public function testGetFieldValueWithCode(): void { $description = new FieldDescription('name', ['code' => 'getFoo']);