I stumbled upon a question regarding Symfony
's DataTransformer
s and how to properly use them. While I know how to implement and add them to my form field, I was wondering how DataTransformer
s are supposed to be combined with Constraint
s.
The following code shows my use case.
<?php
namespace AppBundle\Form;
use AppBundle\Form\DataTransformer\Consent\ConsentTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\IsTrue;
class ConsentTestForm extends AbstractType
{
/** @var ConsentTransformer $consentTransformer */
private $consentTransformer;
/**
* ConsentTestForm constructor.
* @param ConsentTransformer $consentTransformer
*/
public function __construct(ConsentTransformer $consentTransformer)
{
$this->consentTransformer = $consentTransformer;
}
/**
* @inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('accountConsent', CheckboxType::class, [
'constraints' => [
new IsTrue()
]
]);
$builder->get('accountConsent')->addModelTransformer($this->consentTransformer);
$builder->add('submit', SubmitType::class);
}
}
<?php
class User extends Concrete implements \Pimcore\Model\DataObject\DirtyIndicatorInterface
{
protected $accountConsent;
/**
* ...
*/
public function getAccountConsent () {
// ...
}
/**
* ...
*/
public function setAccountConsent ($accountConsent) {
// ...
}
}
A lot of code was omitted for the sake of brevity. The model is a Pimcore class.
<?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 aConsent
and vice-versa.
<?php
namespace AppBundle\Controller;
use AppBundle\Form\ConsentTestForm;
use AppBundle\Model\DataObject\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class TestController
* @package AppBundle\Controller
*
* @Route("/test")
*/
class TestController extends AbstractFrontendController
{
/**
* @Route("/form")
* @param Request $request
* @return Response
*/
public function formAction(Request $request)
{
$user = new User();
$form = $this->createForm(ConsentTestForm::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
p_r("VALID");
p_r($user);
} else {
p_r("NOT VALID");
}
};
return $this->renderTemplate(':Test:form.html.twig', [
"form" => $form->createView()
]);
}
}
Note how a
new User()
is passed as entity in order to automatically populate it with the submitted values.
{{ form(form) }}
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 User
s Consent
into a boolean
.
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.
When 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 accountConsent
field 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'sConsent
class.
All of this leaves me with the question: how do I properly combine the IsTrue
-constraint with my ConsentDataTransformer
?
The problem with the validation is you are trying to validate object as boolean type. Constraints are always executed when you try to validate and transformers when you submit the form. So you have already transformed the data and that is why IsBool
validation fails because the value is in type Consent object; not boolean.
To solve this you have to create new validation constraint overwriting IsTrue.
<?php
namespace App\Form\Validator;
use Symfony\Component\Validator\Constraints\IsTrue;
class IsConsented extends IsTrue
{
public $message = 'You need to consent!';
}
And a validator on the same namespace;
<?php
namespace App\Form\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\IsTrueValidator;
class IsConsentedValidator extends IsTrueValidator
{
public function validate($value, Constraint $constraint)
{
return parent::validate($value->getConsent(), $constraint);
}
}
Then you need to change your IsTrue
constraint with IsConsented
as follows;
<?php
namespace App\Form;
use App\Entity\User;
use App\Form\DataTransformer\ConsentTransformer;
use App\Form\Validator\IsConsented;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ConsentTestFormType extends AbstractType
{
/** @var ConsentTransformer $consentTransformer */
private $consentTransformer;
/**
* ConsentTestForm constructor.
* @param ConsentTransformer $consentTransformer
*/
public function __construct(ConsentTransformer $consentTransformer)
{
$this->consentTransformer = $consentTransformer;
}
/**
* @inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('accountConsent', CheckboxType::class, [
'constraints' => [
new IsConsented()
]
]);
$builder->get('accountConsent')->addModelTransformer($this->consentTransformer);
$builder->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
That is it. Your form is valid now. The output should look like;
FormController.php on line 30:
"VALID"