Imagine example form in Symfony:
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('email', EmailType::class, [
'constraints' =>
new NotBlank(),
new IsUnique(),
],
])
->add('password', PasswordType::class, [
'constraints' =>
new NotBlank(),
new IsStrongEnough(),
],
])
}
Now when I submit the form and make sure it's valid, I'd like the $form->getData()
to return my DTO called CreateAccountCommand
:
final class CreateAccountCommand
{
private $email;
private $password;
public function __construct(string $email, string $password)
{
$this->email = $email;
$this->password = $password;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
{
return $this->password;
}
}
Example controller:
$form = $this->formFactory->create(CreateAccountForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->commandBus->dispatch($form->getData());
return new JsonResponse([]);
}
I cannot use this class directly using the data_class
, because the form obviously expects the model to have setters that allow null values. The form itself works perfectly well, so is the validation.
I tried using the Data mapper method, but the mapFormsToData
method is invoked, before the validation.
Is this possible at all? Or am I supposed to get the data as array and create the object outside the form?
Here is how I'm used to handle form with Symfony 4.1
Let's assume you want a simple add form
The Form
class CreateAccountForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder)
{
//same method than yours
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'App\Entity\CreateAccountCommand'));
}
}
The controller
public function add(Request $request)
{
$createAccountCommand = new CreateAccountCommand();
//The object is "injected" to the form, that way, it's mapped when submitted
$form = $this->get('form.factory')->create(CreateAccountForm::class, $createAccountCommand);
//Form was submitted
if ($request->isMethod('POST') && $form->handleRequest($request)->isValid())
{
//no need to getData(), the object $createAccountCommand is directly mapped to the form and can be used as is
$em = $this->getDoctrine()->getManager();
//persist, convert to json, or whatever
$em->persist($createAccountCommand);
$em->flush();
return ($this->redirectToRoute('someRoute'));
}
//form not submitted, or has been submit with errors (isValid() == false)
return ($this->render('account/add.html.twig',
array('form' => $form->createView())));
}
There is a way, but it is way too complex for what you want to do. You'll need to use Datamapper for this.
I know this is not the best solution, but thesimplest solution would be (in my opinion) adding setters to your model class and allow null values (SOLUTION1). Another solution would be using another object for the form, and build your command after submission ( and you don't need to modify your Command - SOLUTION2).
public function handleForm(Request $request)
{
/// SOLUTION 1
$form = $this->createForm(RegisterFormType::class, new CreateAccountCommand());
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$command = $form->getData();
// do whatever you want
}
// ...
// SOLUTION 2
$obj = new \stdClass();
$obj->login = '';
$obj->password = '';
$form = $this->createForm(LoginFormType::class, $obj);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$command = new CreateAccountCommand($data->login, $data->password);
// do whatever you want
}
}
With this model:
final class CreateAccountCommand
{
//// ...
/**
* @param string $email
*/
public function setEmail(string $email): void
{
$this->email = $email;
}
/**
* @param string $password
*/
public function setPassword(string $password): void
{
$this->password = $password;
}
}