Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Transformers in list fields editable #5937

Merged
merged 1 commit into from
Sep 13, 2020

Conversation

peter-gribanov
Copy link
Contributor

Subject

Allow use Symfony Data Transformers in configureListFields() like:

class TestimonialAdmin extends BaseAdmin
{
    // ...

    protected function configureListFields(ListMapper $list_mapper): void
    {
        $list_mapper
            ->add('permission', 'choice', [
                'label' => 'Permission',
                'editable' => true,
                'choices' => Permission::change(),
                'template' => '@Frontend/Testimonial/admin/list_permission.html.twig',
                'catalogue' => 'messages',
                'data_transformer' => new PermissionDataTransformer(),
            ]);
    }
}

Closes #5693

Changelog

### Fixed
- Allow use Symfony Data Transformers in list fields editable


// Handle boolean type transforming the value into a boolean
if ('boolean' === $fieldDescription->getType()) {
$data_transformer = new BooleanToStringTransformer('1');
Copy link
Contributor Author

@peter-gribanov peter-gribanov Mar 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we need a date transformer here. Using a Symfony BooleanToStringTransformer here may break BC. At least the tests use a non-string.

We can use CallbackTransformer here, so as not to create custom transformer.

$data_transformer = new CallbackTransformer(static function ($value): string {
    return (string) (int) $value;
}, static function ($value): ?bool {
    return filter_var($value, FILTER_VALIDATE_BOOLEAN);
});

@peter-gribanov
Copy link
Contributor Author

Try rebuild

core23
core23 previously approved these changes Mar 8, 2020
OskarStark
OskarStark previously approved these changes Mar 8, 2020
Copy link
Member

@OskarStark OskarStark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any docs changes needed?

@peter-gribanov
Copy link
Contributor Author

@OskarStark this bug fix works more like a feature. It would be nice to document this feature.

@peter-gribanov
Copy link
Contributor Author

@OskarStark i add example here a little bit later.

@peter-gribanov
Copy link
Contributor Author

Try rebuild

@@ -164,4 +161,37 @@ public function __invoke(Request $request): JsonResponse

return new JsonResponse($content, Response::HTTP_OK);
}

private function getFieldDataTransformer(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer a more generic way like a DataTransformerResolver. This way we can add new transformers more easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you suggest just putting this method in a separate class?

Copy link
Contributor Author

@peter-gribanov peter-gribanov Mar 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

final class DataTransformerResolver
{
    private $modelManager;

    public function __construct($modelManager)
    {
        $this->modelManager = $modelManager;
    }

    public function resolve(FieldDescriptionInterface $fieldDescription): ?DataTransformerInterface
    {
        $dataTransformer = $fieldDescription->getOption('data_transformer');

        // allow override transformers for 'date', 'boolean' and 'choice' field types
        if ($dataTransformer instanceof DataTransformerInterface) {
            return $dataTransformer;
        }

        // Handle date type has setter expect a DateTime object
        if ('date' === $fieldDescription->getType()) {
            return new DateTimeToStringTransformer();
        }

        // Handle boolean type transforming the value into a boolean
        if ('boolean' === $fieldDescription->getType()) {
            return new BooleanToStringTransformer('1');
        }

        // Handle entity choice association type, transforming the value into entity
        if ('choice' === $fieldDescription->getType()) {
            $className = $fieldDescription->getOption('class');

            if (null !== $className && $className === $fieldDescription->getTargetEntity()) {
                return new ModelToIdTransformer($this->modelManager, $className);
            }
        }

        return null;
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe you are interested in the option with individual services that are binded into a resolver?

Copy link
Contributor Author

@peter-gribanov peter-gribanov Mar 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

final class ResolverFieldDataTransformer implements FieldDataTransformer
{
    private $dataTransformers;

    public function __construct(array $dataTransformers)
    {
        $this->dataTransformers = $dataTransformers;
    }

    public function transform($value, FieldDescriptionInterface $fieldDescription)
    {
        $dataTransformer = $fieldDescription->getOption('data_transformer');

        // allow override predefined transformers
        if ($dataTransformer instanceof DataTransformerInterface) {
            return $dataTransformer->reverseTransform($value);
        }

        $type = $fieldDescription->getType();

        if (array_key_exists($type, $this->dataTransformers)) {
            return $this->dataTransformers[$type]->transform($value, $fieldDescription);
        }

        return $value;
    }
}

final class ChoiceFieldDataTransformer implements FieldDataTransformer
{
    private $modelManager;

    public function __construct($modelManager)
    {
        $this->modelManager = $modelManager;
    }

    public function transform($value, FieldDescriptionInterface $fieldDescription)
    {
        $className = $fieldDescription->getOption('class');

        if (null !== $className && $className === $fieldDescription->getTargetEntity()) {
            return (new ModelToIdTransformer($this->modelManager, $className))->reverseTransform($value);
        }

        return $value;
    }
}

$resolver = new ResolverFieldDataTransformer([
    'choice' => new ChoiceFieldDataTransformer($modelManager),
]);

$value = $admin->getModelManager()->find($fieldDescription->getOption('class'), $value);

if (!$value) {
if (!$value && 'choice' === $fieldDescription->getType()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@core23 i am very confused by the tight binding to the field type here. I understand why this is done, but it still looks like a dirty hack.

}

return null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@core23 please review DataTransformerResolver service

@VincentLanglet
Copy link
Member

VincentLanglet commented Aug 31, 2020

@sonata-project/contributors have any news?

Can you fix the phpstan check ?

@sonata-project/contributors @core23 Please review.

@peter-gribanov
Copy link
Contributor Author

peter-gribanov commented Aug 31, 2020

Can you fix the phpstan check ?

@VincentLanglet problem is not on my side.
https://github.com/sonata-project/SonataAdminBundle/blob/3.x/src/Guesser/TypeGuesserChain.php#L63

@VincentLanglet
Copy link
Member

I fixed it ; you just have to rebase now

#6356

VincentLanglet
VincentLanglet previously approved these changes Aug 31, 2020
@VincentLanglet VincentLanglet requested a review from a team September 1, 2020 07:53
@SonataCI
Copy link
Collaborator

SonataCI commented Sep 3, 2020

Could you please rebase your PR and fix merge conflicts?

wbloszyk
wbloszyk previously approved these changes Sep 4, 2020
Copy link
Member

@wbloszyk wbloszyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work.

VincentLanglet
VincentLanglet previously approved these changes Sep 4, 2020
@VincentLanglet
Copy link
Member

Please review @core23. :)

(Or can you dismiss the review @greg0ire if we get no response after some days ?)

@greg0ire greg0ire dismissed core23’s stale review September 5, 2020 09:41

Dismissing it now, but you can wait some days if you want to :)

tests/Form/DataTransformerResolverTest.php Outdated Show resolved Hide resolved
tests/Form/DataTransformerResolverTest.php Outdated Show resolved Hide resolved
allow use data_transformer in SetObjectFieldValueAction

create BooleanToStringTransformer for allows to use non-strings

update SetObjectFieldValueActionTest

use yoda conditions

fix errors in HelperControllerTest

test BooleanToStringTransformer

allow override transformers for 'date', 'boolean' and 'choice' field types

mark BooleanToStringTransformer and BooleanToStringTransformer classes as final

add example of using the data_transformer option in docs

add full docs about Symfony Data Transformers

optimize resolve Data Transformer

fix docs

create DataTransformerResolver service

add type hint for BooleanToStringTransformer::$trueValue

allow add a custom global transformers

field type should be a string

correct default value for $globalCustomTransformers

correct test DataTransformerResolverTest::testAddCustomGlobalTransformer()

add BC support usage of DataTransformerResolver

Update tests/Action/SetObjectFieldValueActionTest.php

Update tests/Form/DataTransformer/BooleanToStringTransformerTest.php

Update tests/Form/DataTransformerResolverTest.php

Update src/Action/SetObjectFieldValueAction.php

change "entity" word to "model" in documentations

change deprecated error message

add datetime in editable date form types

correct test transform datetime and date form types

test DateTime object in assertSame()

fix typo

restore getTemplate() return value in SetObjectFieldValueActionTest

use Yoda conditions

lazy-load predefined data transformers

add DataTransformerResolverInterface

use constants for determinate a field type

test laze-load data transformers

test usage DataTransformerResolver::addCustomGlobalTransformer()

create simple function in DataTransformerResolverTest

Process deprecation of FieldDescriptionInterface::getTargetEntity()

Use FieldDescriptionInterface::getTargetModel if exists sonata-project#6208

change usage getTargetEntity() -> getTargetModel() in DataTransformerResolverTest

merge changes from PR sonata-project#6167

register BooleanToStringTransformer as a service

merge changes from PR sonata-project#6144

merge changes from PR sonata-project#6284

compare date with time in DataTransformerResolverTest
Copy link
Member

@wbloszyk wbloszyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use DateTimeInterface, anyway IMO we can accept this.


$resultDate = $dataTransformer->reverseTransform($value);

$this->assertInstanceOf(\DateTime::class, $resultDate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->assertInstanceOf(\DateTime::class, $resultDate);
$this->assertInstanceOf(\DateTimeInterface::class, $resultDate);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the transformer always returns the \DateTime. If we expect the interface in tests, we may not notice how we break BC by replacing \DateTime with \DateTimeImmutable in the transformer.

$resultDate = $dataTransformer->reverseTransform($value);

$this->assertInstanceOf(\DateTime::class, $resultDate);
$this->assertSame($expectedDate->format(\DateTime::ATOM), $resultDate->format(\DateTime::ATOM));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->assertSame($expectedDate->format(\DateTime::ATOM), $resultDate->format(\DateTime::ATOM));
$this->assertSame($expectedDate->format(\DateTimeInterface::ATOM), $resultDate->format(\DateTime::ATOM));

@OskarStark
Copy link
Member

@core23 @phansys @jordisala1991 can you please give us a final review? Thanks

@VincentLanglet
Copy link
Member

Thanks !

@VincentLanglet VincentLanglet merged commit c4d0026 into sonata-project:3.x Sep 13, 2020
@peter-gribanov peter-gribanov deleted the data_transformer branch September 14, 2020 06:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Data Transformers in list fields editable
8 participants