3

I would like to have a page containing two forms

example of what I want

Those two forms update the same entity: Account.

What I did:

  • Created two forms (AccountGeneralDataType.php & AccountPasswordType.php)
  • Added them to the AccountController.php under the editAction method

AccountGeneralDataType.php

<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class AccountGeneralDataType extends AbstractType
{

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        /**
         * todo: Missing avatar upload
         */
        $builder
            ->add('username', TextType::class, [
                'label' => 'Username'
            ])
            ->add('email', EmailType::class, [
                'label' => 'Email'
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Save changes',
                'attr' => [
                    'class' => 'btn btn-outline-primary float-right'
                ]
            ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\Account'
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_account';
    }

}

AccountPasswordType.php

<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Validator\Constraints\NotBlank;

class AccountPasswordType extends AbstractType
{

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('password', PasswordType::class, [
                'label' => 'Old password',
            ])
            ->add('new-password', RepeatedType::class, [
                'label' => 'New password',
                'type' => PasswordType::class,
                'invalid_message' => 'The new password fields must match.',
                'options' => ['attr' => ['class' => 'password-field']],
                'required' => true,
                'first_options' => [
                    'label' => 'New password'
                ],
                'second_options' => [
                    'label' => 'Repeat password'
                ],
                'constraints' => [
                    new NotBlank(),
                ],
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Update password',
                'attr' => [
                    'class' => 'btn btn-outline-primary float-right'
                ]
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
//            'data_class' => 'AppBundle\Entity\Account'
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_account';
    }

}

AccountController.php

public function editAction(Request $request, Account $account)
{
    $originalAccount = new Account();
    $originalAccount->setEmail($account->getEmail());
    $originalAccount->setUsername($account->getUsername());
    $originalAccount->setAvatar($account->getAvatar());

    /**
     * Form to edit general information
     */
    $editAccountForm = $this->createForm('AppBundle\Form\AccountGeneralDataType', $account);
    $editAccountForm->handleRequest($request);

    /**
     * Form to edit password
     */
    $editPasswordForm = $this->createForm('AppBundle\Form\AccountPasswordType', []);
    $editPasswordForm->handleRequest($request);

    /**
     * Form to delete the account
     */
    $deleteForm = $this->createDeleteForm($account);

    if ($editAccountForm->isSubmitted() && $editAccountForm->isValid()) {
        $this->getDoctrine()->getManager()->flush();

        return $this->redirectToRoute('account_edit', array('username' => $account->getUsername()));
    } else if ($editPasswordForm->isSubmitted() && $editPasswordForm->isValid()) {
        $account = $originalAccount;

        $this->getDoctrine()->getManager()->flush();
        return $this->redirectToRoute('account_edit', [
                'username' => $originalAccount->getUsername()
        ]);
    }

What's the problem?

  • validation of the Password form does not works (two different fields don't trigger the 'fields are different')
  • $account is not properly set if I submit the password field, resulting in a Doctrine error saying the query is missing parameters

I think my way of doing it isn't the good one but I didn't find any clean documentation / good dev post about how to have 2 forms editing the same entity on the same page.

darckcrystale
  • 1,582
  • 2
  • 18
  • 40
  • Why the requirement for having two on the same page? This would be very simple if it were one (or two) forms that shared a "submit" button and edited the same entity. – Ollie in PGH Jan 11 '18 at 12:52
  • It's a UX choice. As same as GitHub page uses multiple forms in the Profile edition page, for example. – darckcrystale Jan 11 '18 at 13:04
  • 1
    After a quick reading, I'd say it should work, I'm using something very similar in my project (except the FormTypes are not linked to entities). Unfortunately I don't have time right now to check your code in details, but I'll try to have a look tonight. In the meantime, maybe my old answer here can help you (I know I'm not supposed to write links, but anyway...): https://stackoverflow.com/questions/47893964/symfony-form-contains-optional-additional-form-how-to-submit-persist-both/47895154#47895154 – Nicolas Jan 12 '18 at 13:43
  • Thank you @Nicolas, it really helped me! – darckcrystale Jan 26 '18 at 18:37

3 Answers3

2

Ok, I solved it!

What did I do:

First, I created two forms in project/src/AppBundle/Form/:

AccountGeneralDataType.php

<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class AccountGeneralDataType extends AbstractType
{

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        /**
         * todo: Missing avatar upload
         */
        $builder
            ->add('username', TextType::class, [
                'label' => 'Username',
                'required' => true,
            ])
            ->add('email', EmailType::class, [
                'label' => 'Email',
                'required' => true,
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Save changes',
                'attr' => [
                    'class' => 'btn btn-outline-primary float-right',
                ]
            ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\Account',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_general_data_account';
    }

}

Don't forget to set a unique label in the getBlockPrefix()!

AccountPasswordType.php

<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Validator\Constraints\NotBlank;

class AccountPasswordType extends AbstractType
{

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('old-password', PasswordType::class, [
                'label' => 'Old password',
                'mapped' => false,
            ])
            ->add('new-password', RepeatedType::class, [
                'label' => 'New password',
                'mapped' => false,
                'type' => PasswordType::class,
                'invalid_message' => 'The new password fields must match.',
                'options' => ['attr' => ['class' => 'password-field']],
                'required' => true,
                'first_options' => [
                    'label' => 'New password'
                ],
                'second_options' => [
                    'label' => 'Repeat password'
                ],
                'constraints' => [
                    new NotBlank(),
                ],
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Update password',
                'attr' => [
                    'class' => 'btn btn-outline-primary float-right'
                ]
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'allow_extra_fields' => true,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_change_password';
    }

}

Don't forget to set a unique label in the getBlockPrefix()!

The AccountPasswordType does not use a data_class (which is set in the configureOptions() method) because I don't fill automatically an object with it.

Second, I called them in my controller:

AccountController.php

<?php

namespace AppBundle\Controller;

use AppBundle\Entity\Account;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\FormError;

/**
 * Account controller.
 *
 * @Route("account")
 */
class AccountController extends Controller
{
    /**
     * Displays forms to edit an existing account entity.
     *
     * @Route("/edit/{username}", name="account_edit")
     * @Method({"GET", "POST"})
     */
    public function editAction(Request $request, Account $account)
    {
        /**
         * Form to edit general information
         */
        $editAccountGeneralInformationForm = $this->createForm('AppBundle\Form\AccountGeneralDataType', $account);
        $editAccountGeneralInformationForm->handleRequest($request);

        /**
         * Form to edit password
         */
        $editAccountPasswordForm = $this->createForm('AppBundle\Form\AccountPasswordType', []);
        $editAccountPasswordForm->handleRequest($request);

        // Handle the account form
        if ($editAccountGeneralInformationForm->isSubmitted() && !$editAccountGeneralInformationForm->isValid())
        {
            $session->getFlashBag()->add('danger', 'Your iinformation have not been updated, please retry.');
        }

        if ($editAccountGeneralInformationForm->isSubmitted() && $editAccountGeneralInformationForm->isValid())
        {
            $this->getDoctrine()->getManager()->flush();

            $session->getFlashBag()->add('success', 'Your information have been updated.');
        }


        // Handle the password form
        if ($editAccountPasswordForm->isSubmitted() && !$editAccountPasswordForm->isValid())
        {
            $session->getFlashBag()->add('danger', 'Your password have not been updated, please retry.');
        }

        if ($editAccountPasswordForm->isSubmitted() && $editAccountPasswordForm->isValid())
        {
            $oldUserPassword = $account->getPassword();
            $oldPasswordSubmitted = $editAccountPasswordForm['old-password']->getData();
            $newPasswordSubmitted = $editAccountPasswordForm['new-password']->getData();

            if (password_verify($oldPasswordSubmitted, $oldUserPassword))
            {
                $newPassword = password_hash($newPasswordSubmitted, PASSWORD_BCRYPT, ['cost' => 15]);
                $account->setPassword($newPassword);
                $this->getDoctrine()->getManager()->flush();

                $session->getFlashBag()->add('success', 'Your password have been updated.');
            }
            else
            {
                $editAccountPasswordForm->get('old-password')->addError(new FormError('Incorrect password.'));
                $session->getFlashBag()->add('danger', 'Your password have not been updated, please retry.');
            }
        }

        return $this->render('account/edit.html.twig', [
                'account' => $account,
                'edit_form' => $editAccountGeneralInformationForm->createView(),
                'edit_password_form' => $editAccountPasswordForm->createView(),
        ]);
    }
}

Using $foobarForm->isSubmitted(), we can know if the form have been submitted.

My biggest problem was that the two Type classes for my forms had the same name (defined in getBlockPrefix()) so when I submitted the second one, it was the first one which thrown errors.

darckcrystale
  • 1,582
  • 2
  • 18
  • 40
  • Thanks - was struggling with submitting separate forms (JS separated div toggle) as submitting either fired the form validation on both. The getBlockPrefix resolved that! – James Mar 22 '20 at 02:53
  • A tip in the controller BTW, I did this `if ($formA->isSubmitted() || $formB->isSubmitted()) {` then you can do `$submittedForm = $formA->isSubmitted() ? $formA : $formB;` – James Mar 22 '20 at 02:55
-1

You can render the two view in another one with twig.

First, create a new view and it's action in a new controller. Register this controller routing into your routing file.

Then use this to render your two form view into the new created view

{{ render(controller('AppBundle:Controller:Action')) }}

This should do the trick... ;)

Preciel
  • 2,666
  • 3
  • 20
  • 45
-2

If you're going to use one page and two forms they'll be separate in the Request.

For example, you'll have to evaluate which form was submitted like:

if ($request->request->has('editAccountForm')) {
    // handle the account form 
}

if ($request->request->has('editPasswordForm')) {
    // handle the password form  
}

There's a blog post for Symfony 2 which covers this. It will be similar for Symfony 3.

Ollie in PGH
  • 2,559
  • 2
  • 16
  • 19