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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions docs/reference/action_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,115 @@ Available types and associated options
| ``TemplateRegistry::TYPE_*`` | | See :doc:`Field Types <field_types>` |
+--------------------------------------+---------------------+-----------------------------------------------------------------------+

Symfony Data Transformers
^^^^^^^^^^^^^^^^^^^^^^^^^

If the model field has a limited list of values (enumeration), it is convenient to use a value object to control
the available values. For example, consider the value object of moderation status with the following values:
``awaiting``, ``approved``, ``rejected``::

final class ModerationStatus
{
public const AWAITING = 'awaiting';
public const APPROVED = 'approved';
public const REJECTED = 'rejected';

private static $instances = [];

private string $value;

private function __construct(string $value)
{
if (!array_key_exists($value, self::choices())) {
throw new \DomainException(sprintf('The value "%s" is not a valid moderation status.', $value));
}

$this->value = $value;
}

public static function byValue(string $value): ModerationStatus
{
// limitation of count object instances
if (!isset(self::$instances[$value])) {
self::$instances[$value] = new static($value);
}

return self::$instances[$value];
}

public function getValue(): string
{
return $this->value;
}

public static function choices(): array
{
return [
self::AWAITING => 'moderation_status.awaiting',
self::APPROVED => 'moderation_status.approved',
self::REJECTED => 'moderation_status.rejected',
];
}

public function __toString(): string
{
return self::choices()[$this->value];
}
}

To use this Value Object in the _`Symfony Form`: https://symfony.com/doc/current/forms.html component, we need a
_`Data Transformer`: https://symfony.com/doc/current/form/data_transformers.html ::

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

final class ModerationStatusDataTransformer implements DataTransformerInterface
{
public function transform($value): ?string
{
$status = $this->reverseTransform($value);

return $status instanceof ModerationStatus ? $status->value() : null;
}

public function reverseTransform($value): ?ModerationStatus
{
if (null === $value || '' === $value) {
return null;
}

if ($value instanceof ModerationStatus) {
return $value;
}

try {
return ModerationStatus::byValue($value);
} catch (\Throwable $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
}
}

For quick moderation of objects, it is convenient to do this on the page for viewing all objects. But if we just
indicate the field as editable, then when editing we get in the object a string with the value itself (``awaiting``,
``approved``, ``rejected``), and not the Value Object (``ModerationStatus``). To solve this problem, you must specify
the Data Transformer in the ``data_transformer`` field so that it correctly converts the input data into the data
expected by your object::

// ...

protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('moderation_status', 'choice', [
'editable' => true,
'choices' => ModerationStatus::choices(),
'data_transformer' => new ModerationStatusDataTransformer(),
])
;
}


wbloszyk marked this conversation as resolved.
Show resolved Hide resolved
Customizing the query used to generate the list
-----------------------------------------------

Expand Down
70 changes: 35 additions & 35 deletions src/Action/SetObjectFieldValueAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

namespace Sonata\AdminBundle\Action;

use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\AdminBundle\Form\DataTransformerResolver;
use Sonata\AdminBundle\Form\DataTransformerResolverInterface;
use Sonata\AdminBundle\Templating\TemplateRegistry;
use Sonata\AdminBundle\Twig\Extension\SonataAdminExtension;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -42,7 +44,16 @@ final class SetObjectFieldValueAction
*/
private $validator;

public function __construct(Environment $twig, Pool $pool, $validator)
/**
* @var DataTransformerResolver
*/
private $resolver;

/**
* @param ValidatorInterface $validator
* @param DataTransformerResolver|null $resolver
*/
public function __construct(Environment $twig, Pool $pool, $validator, $resolver = null)
{
// NEXT_MAJOR: Move ValidatorInterface check to method signature
if (!($validator instanceof ValidatorInterface)) {
Expand All @@ -52,9 +63,22 @@ public function __construct(Environment $twig, Pool $pool, $validator)
ValidatorInterface::class
));
}

// NEXT_MAJOR: Move DataTransformerResolver check to method signature
if (!$resolver instanceof DataTransformerResolverInterface) {
@trigger_error(sprintf(
'Passing other type than %s in argument 4 to %s() is deprecated since sonata-project/admin-bundle 3.x and will throw %s exception in 4.0.',
DataTransformerResolverInterface::class,
__METHOD__,
\TypeError::class
), E_USER_DEPRECATED);
$resolver = new DataTransformerResolver();
}

$this->pool = $pool;
$this->twig = $twig;
$this->validator = $validator;
$this->resolver = $resolver;
}

/**
Expand Down Expand Up @@ -120,43 +144,25 @@ public function __invoke(Request $request): JsonResponse
$propertyPath = new PropertyPath($field);
}

// Handle date type has setter expect a DateTime object
if ('' !== $value && TemplateRegistry::TYPE_DATE === $fieldDescription->getType()) {
$inputTimezone = new \DateTimeZone(date_default_timezone_get());
$outputTimezone = $fieldDescription->getOption('timezone');
if ('' === $value) {
$this->pool->getPropertyAccessor()->setValue($object, $propertyPath, null);
} else {
$dataTransformer = $this->resolver->resolve($fieldDescription, $admin->getModelManager());

if ($outputTimezone && !$outputTimezone instanceof \DateTimeZone) {
$outputTimezone = new \DateTimeZone($outputTimezone);
if ($dataTransformer instanceof DataTransformerInterface) {
$value = $dataTransformer->reverseTransform($value);
}

$value = new \DateTime($value, $outputTimezone ?: $inputTimezone);
$value->setTimezone($inputTimezone);
}

// Handle boolean type transforming the value into a boolean
if ('' !== $value && TemplateRegistry::TYPE_BOOLEAN === $fieldDescription->getType()) {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
}

// Handle entity choice association type, transforming the value into entity
if ('' !== $value
&& TemplateRegistry::TYPE_CHOICE === $fieldDescription->getType()
&& null !== $fieldDescription->getOption('class')
// NEXT_MAJOR: Replace this call with "$fieldDescription->getOption('class') === $fieldDescription->getTargetModel()".
&& $this->hasFieldDescriptionAssociationWithClass($fieldDescription, $fieldDescription->getOption('class'))
) {
$value = $admin->getModelManager()->find($fieldDescription->getOption('class'), $value);

if (!$value) {
if (!$value && TemplateRegistry::TYPE_CHOICE === $fieldDescription->getType()) {
return new JsonResponse(sprintf(
'Edit failed, object with id: %s not found in association: %s.',
$originalValue,
$field
), Response::HTTP_NOT_FOUND);
}
}

$this->pool->getPropertyAccessor()->setValue($object, $propertyPath, '' !== $value ? $value : null);
$this->pool->getPropertyAccessor()->setValue($object, $propertyPath, $value);
}

$violations = $this->validator->validate($object);

Expand All @@ -181,10 +187,4 @@ public function __invoke(Request $request): JsonResponse

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

private function hasFieldDescriptionAssociationWithClass(FieldDescriptionInterface $fieldDescription, string $class): bool
{
return (method_exists($fieldDescription, 'getTargetModel') && $class === $fieldDescription->getTargetModel())
|| $class === $fieldDescription->getTargetEntity();
}
}
26 changes: 23 additions & 3 deletions src/Controller/HelperController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use Sonata\AdminBundle\Action\SetObjectFieldValueAction;
use Sonata\AdminBundle\Admin\AdminHelper;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\AdminBundle\Form\DataTransformerResolver;
use Sonata\AdminBundle\Form\DataTransformerResolverInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -64,9 +66,15 @@ class HelperController
protected $validator;

/**
* @param ValidatorInterface $validator
* @var DataTransformerResolver
*/
public function __construct(Environment $twig, Pool $pool, AdminHelper $helper, $validator)
private $resolver;

/**
* @param ValidatorInterface $validator
* @param DataTransformerResolver|null $resolver
*/
public function __construct(Environment $twig, Pool $pool, AdminHelper $helper, $validator, $resolver = null)
{
// NEXT_MAJOR: Move ValidatorInterface check to method signature
if (!($validator instanceof ValidatorInterface)) {
Expand All @@ -77,10 +85,22 @@ public function __construct(Environment $twig, Pool $pool, AdminHelper $helper,
));
}

// NEXT_MAJOR: Move DataTransformerResolver check to method signature
if (!$resolver instanceof DataTransformerResolverInterface) {
@trigger_error(sprintf(
'Passing other type than %s in argument 4 to %s() is deprecated since sonata-project/admin-bundle 3.x and will throw %s exception in 4.0.',
DataTransformerResolverInterface::class,
__METHOD__,
\TypeError::class
), E_USER_DEPRECATED);
$resolver = new DataTransformerResolver();
}

$this->twig = $twig;
$this->pool = $pool;
$this->helper = $helper;
$this->validator = $validator;
$this->resolver = $resolver;
}

/**
Expand Down Expand Up @@ -124,7 +144,7 @@ public function getShortObjectDescriptionAction(Request $request)
*/
public function setObjectFieldValueAction(Request $request)
{
$action = new SetObjectFieldValueAction($this->twig, $this->pool, $this->validator);
$action = new SetObjectFieldValueAction($this->twig, $this->pool, $this->validator, $this->resolver);

return $action($request);
}
Expand Down
45 changes: 45 additions & 0 deletions src/Form/DataTransformer/BooleanToStringTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\AdminBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

/**
* This is analog of Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer
* which allows you to use non-strings in reverseTransform() method.
*
* @author Peter Gribanov <info@peter-gribanov.ru>
*/
final class BooleanToStringTransformer implements DataTransformerInterface
{
/**
* @var string
*/
private $trueValue;
peter-gribanov marked this conversation as resolved.
Show resolved Hide resolved

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

public function transform($value): ?string
{
return $value ? $this->trueValue : null;
}

public function reverseTransform($value): bool
{
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
}
Loading