Quantcast
Channel: Active questions tagged symfony4 - Stack Overflow
Viewing all articles
Browse latest Browse all 3925

Data Transformer vs.Constraints

$
0
0

I stumbled upon a question regarding Symfony's DataTransformers and how to properly use them. While I know how to implement and add them to my form field, I was wondering how DataTransformers are supposed to be combined with Constraints.

The following code shows my use case.

The Form

<?php

namespace AppBundle\Form;

use Passioneight\Bundle\FormBuilderBundle\Form\AbstractForm;
use Passioneight\Bundle\FormBuilderBundle\Form\DataTransformer\Consent\ConsentTransformer;
use Passioneight\Bundle\FormBuilderBundle\Form\Field\Button\SubmitButton;
use Passioneight\Bundle\FormBuilderBundle\Form\Field\Checkbox\AccountConsentField;
use Passioneight\Bundle\FormBuilderBundle\Form\Field\Text\EmailField;
use Passioneight\Bundle\FormBuilderBundle\Form\Field\Text\RepeatedField\RepeatedPasswordField;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class RegistrationForm extends AbstractForm
{
    const OPTION_TERMS_AND_CONDITIONS_HREF = "terms-and-conditions-href";

    /** @var ConsentTransformer $consentTransformer */
    private $consentTransformer;

    /**
     * RegistrationForm constructor.
     * @param ConsentTransformer $consentTransformer
     */
    public function __construct(ConsentTransformer $consentTransformer)
    {
        $this->consentTransformer = $consentTransformer;
    }

    /**
     * @inheritDoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addField(new EmailField());
        $builder->addField(new RepeatedPasswordField());
        $builder->addField(new AccountConsentField([
            'label_translation_parameters' => [
                '{{ href }}' => $options[self::OPTION_TERMS_AND_CONDITIONS_HREF],
                '{{ href-class }}' => 'text-primary-hover'
            ],
        ]));
        $builder->get(AccountConsentField::NAME)->addModelTransformer($this->consentTransformer);

        $builder->addField(new SubmitButton([
            'label_format' => 'form.registration.%name%',
            'attr' => [
                'class' => 'btn-primary btn-raised m-0'
            ]
        ]));
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        parent::configureOptions($resolver);
        $this->allowOption($resolver, self::OPTION_TERMS_AND_CONDITIONS_HREF, "#");
    }
}

Wondering about the way form fields are added to the form? That's because I'm using my bundle. It's essentially the same as $builder->addField(...);, only that any predefined fields in the bundle can be added in a more convenient way.

The Model

<?php

class User extends Concrete implements \Pimcore\Model\DataObject\DirtyIndicatorInterface
{
    protected $accountConsent;

    /**
    * Get accountConsent - Account Consent
    * @return \Pimcore\Model\DataObject\Data\Consent
    */
    public function getAccountConsent () {
        if($this instanceof PreGetValueHookInterface && !\Pimcore::inAdmin()) { 
            $preValue = $this->preGetValue("accountConsent"); 
            if($preValue !== null) { 
                return $preValue;
            }
        } 

        $data = $this->accountConsent;

        if ($data instanceof \Pimcore\Model\DataObject\Data\EncryptedField) {
            return $data->getPlain();
        }

        return $data;
    }

    /**
    * Set accountConsent - Account Consent
    * @param \Pimcore\Model\DataObject\Data\Consent $accountConsent
    * @return \Pimcore\Model\DataObject\User
    */
    public function setAccountConsent ($accountConsent) {
        $fd = $this->getClass()->getFieldDefinition("accountConsent");
        $this->accountConsent = $accountConsent;
        return $this;
    }
}

A lot of code was omitted for the sake of brevity. The model is a Pimcore class, which also holds the code required for mapping the other form fields (such as EmailField).

The DataTransformer

<?php

namespace Passioneight\Bundle\FormBuilderBundle\Form\DataTransformer\Consent;

use Pimcore\Model\DataObject\Data\Consent;
use Symfony\Component\Form\DataTransformerInterface;

class ConsentTransformer implements DataTransformerInterface
{
    /**
     * @inheritDoc
     * @param Consent|null $consent
     */
    public function transform($consent)
    {
        return $consent instanceof Consent && $consent->getConsent();
    }

    /**
     * @inheritDoc
     * @param bool|null $consented
     */
    public function reverseTransform($consented)
    {
        $consent = new Consent();
        $consent->setConsent($consented ?: false);
        return $consent;
    }
}

As you can see any submitted value (i.e., null, true, false) will be converted to a Consent and vice-versa.

Building the Form

<?php

namespace AppBundle\Controller;

use AppBundle\Form\RegistrationForm;
use Pimcore\Model\DataObject\User;
use Passioneight\Bundle\FormBuilderBundle\Service\FormBuilderService;

/**
* Class AuthController
* @package AppBundle\Controller
*
* @Route("/{_locale}")
*/
class AuthController extends AbstractFrontendController
{
    /**
    * @Route("/registration")
    *
    * @param FormBuilderService $formBuilderService
    */
    public function registrationAction(FormBuilderService $formBuilderService)
    {
        $options = [
            RegistrationForm::OPTION_TERMS_AND_CONDITIONS_HREF => "/url/to/TACs",
        ];

        $formEvent = $formBuilderService->buildForm(RegistrationForm::class, new User(), $options);
        $this->view->registrationForm = $formEvent->getFormView();
    }
}

Note how a new User() is passed as entity in order to automatically populate it with the submitted values.

The FormBuilderService is also available in my bundle, as it is just some convience class to avoid doing the same thing over and over again for each form.

The Problem

The form can be built just fine, ultimately, displaying a checkbox with my specified label. Due to the transformer, the checked-state is even correctly displayed, as the transform method converts the Users Consent into a boolean (even if no Consent was set yet).

However, when submitting the form an error is displayed, saying that the account consent is required. While this is fine when it comes to submitting the form without giving consent, it's not quite the desired outcome when acutally consenting.

The submitted value is converted to a Consent, which will then hold the value true. But since the transformation is done before the submitted value is validated the beforementioned error is displayed. This happens, because the AccountConsentField that was added in the form has a Constraint set, namely, IsTrue. Due to this, the IsTrueValidator validates the Consent (instead of the actually submitted value).

Obviously, the IsTrueValidator cannot know about Pimcore's Consent class. So, I've found the following workarounds.

Known Workarounds

  1. Removing the IsTrue-constraint.

    By doing so, the value is not validated anymore, but that's not a good idea - for obvious reasons.

  2. Same as 1., but with the validation in the transformer (i.e., by throwing the TransformationFailedException if the user did not consent).

    This way the value is still validated, but a DataTransformer should not handle validation (which others think too).

  3. Leaving the transformation to the developer.

    Well, obviously I don't want to go with this option, because I want to ease these kind of things for the developers.

All of these workarounds also leave me with the problem of a possible combination of the DataTransformer and any Constraint when a developer is not aware of this issue.

Side-Note: I labled this "workarounds" because these solutions just don't feel like the proper way to go.

The Question

All of this leaves me with the question: how do I properly combine the IsTrue-constraint with my ConsentDataTransformer?


What else I tried

  • Using a NotBlank-constraint instead of the IsTrue-constraint. The reason being that this Constraint allows to define a normalizer, which could transform the Consent back to a boolean for the NotBlankValidatoronly.

    If only the normalizer was called independent of the value's data type (currently only string is supported).

  • Extending the IsTrue-constraint in order to override the validatedBy method.

    While this allows me to create a custom validator that is aware of the Consent class, the developer needs to know which IsTrue-constraint to use, if they decide to customize the AccountConsentField, e.g. by overriding default Constraints. Even though this situation is unlikely; just imagine a different transformer for a field that is likely changed in terms of constraints. Symfony's built-in Validators would most likely behave unexpectedly.

Research

I've already searched for similar issues and found that especially this question seems to be alike (though only in the beginning). Yet, it did not quite answer my question.


Viewing all articles
Browse latest Browse all 3925

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>