Customizing the Forms

What if we wanted to add our own fields to the registration form? Like, what if we needed to add a "First Name" field? No problem!

Adding a firstName field to User

Start in our User class. This is a normal entity... so we can add whatever fields we want, like private $firstName. I'll go to Code->Generate, or Command+N on a Mac, then select "ORM Annoations" to annotate firstName. Then I'll go back to Code->Generate to add the getter and setter methods.

41 lines src/AppBundle/Entity/User.php
<?php
... lines 2 - 11
class User extends BaseUser
{
... lines 14 - 20
/**
* @ORM\Column(type="string")
*/
private $firstName;
... lines 25 - 30
public function getFirstName()
{
return $this->firstName;
}
public function setFirstName($firstName)
{
$this->firstName = $firstName;
}
}

Notice, right now, firstName is not nullable... meaning it's required in the database... and that's fine! If firstName is optional in your app, you can of course add nullable=true. I'm mentioning this for one reason: if you add any required fields to your User, the fos:user:create command will no longer work... because it will create a new User but leave those fields blank. I never use that command in production anyways, but, you've been warned!

Move over to your terminal to generate the migration:

php bin/console doctrine:migrations:diff

That looks right! Run it:

php bin/console doctrine:migrations:migrate

New field added! Now, how can we add it to the form? Simple! We can create our own new form class and tell FOSUserBundle to use it instead.

Creating the Override Form

In src/AppBundle, create a new Form directory and a new class called RegistrationFormType. Extend the normal AbstractType. Then, I'll use Code->Generate Menu or Command+N to override the buildForm() method. Inside, just say $builder->add('firstName').

22 lines src/AppBundle/Form/RegistrationFormType.php
<?php
... line 2
namespace AppBundle\Form;
... lines 4 - 8
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName');
}
... lines 16 - 20
}

In a minute, we'll tell FOSUserBundle to use this form instead of its normal registration form. But... instead of completely replacing the default form, what I really want to do is just add one field to it. Is there a way to extend the existing form?

Extending the Core Form

Totally! And once again, the web debug toolbar can help us out. Mouse over the form icon and click that. This tells us what form is used on this page: it's called RegistrationFormType - the same as our form class!

To build on top of that form, you don't actually extend it. Instead, override a method called getParent(). Inside, we'll return the class that we want to extend. At the top, add use, autocomplete RegistrationFormType from the bundle and put as BaseRegistrationFormType to avoid conflicting.

Now in getParent(), we can say return BaseRegistrationFormType::class.

22 lines src/AppBundle/Form/RegistrationFormType.php
<?php
... lines 2 - 6
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseRegistrationFormType;
... line 8
class RegistrationFormType extends AbstractType
{
... lines 11 - 16
public function getParent()
{
return BaseRegistrationFormType::class;
}
}

And that is it! This form will have the existing fields plus firstName.

Registering the Form with FOSUserBundle

To tell FOSUserBundle about our form, we need to do two things. First, register this as a service. In my app/config/services.yml, add app.form.registration with class set to the RegistrationFormType. It also needs to be tagged with name: form.type.

22 lines app/config/services.yml
... lines 1 - 5
services:
... lines 7 - 17
app.form.registration:
class: AppBundle\Form\RegistrationFormType
tags:
- { name: form.type }

Finally, copy the class name and go into app/config/config.yml. This bundle has a lot of configuration. And at the bottom of the documentation page, you can find a reference called FOSUserBundle Configuration Reference. I'll open it in a new tab.

This is pretty awesome: a full dump of all of the configuration options. Some of these are explained in more detail in other places in the docs, but I love seeing everything right in front of me. And we can see what we're looking for under registration.form.type.

Go back to your editor and add those: registration, form and type. Paste our class name!

85 lines app/config/config.yml
... lines 1 - 74
fos_user:
... lines 76 - 81
registration:
form:
type: AppBundle\Form\RegistrationFormType

And... we're done! Go back to registration and refresh. We got it! And that will save when we submit.

Customizing the Form Order

Why is firstName at the bottom? Well, remember inside of register_content.html.twig, we're using form_widget(form)... which just dumps out the fields in whatever order they were added. Need more control? Cool: remove that and instead use form_row(form.email), form_row(form.username), form_row(form.firstName) and form_row(form.plainPassword).

14 lines app/Resources/FOSUserBundle/views/Registration/register_content.html.twig
... lines 1 - 2
<h1>Register Aquanaut!</h1>
... line 4
{{ form_start(form, {'method': 'post', 'action': path('fos_user_registration_register'), 'attr': {'class': 'fos_user_registration_register'}}) }}
{{ form_row(form.email) }}
{{ form_row(form.username) }}
{{ form_row(form.firstName) }}
{{ form_row(form.plainPassword) }}
... lines 10 - 12
{{ form_end(form) }}

If you're not sure what the field names are called, again, use your web debug toolbar for the form: it shows you everything.

Refresh that page! Yes! First Name is exactly where we want it to be. Next, what about the username? Could we remove that if our app only needs an email?

Leave a comment!

  • 2017-10-03 Robert

    Hi Ryan,

    Thank you for confirming that. I will definitely go with that solution. Thanks for you time and effort!

    Regards,
    Rob

  • 2017-10-03 weaverryan

    Yo Robert!

    Yea, the form errors are a huge pain when you're working with an API :/. So, I feel you on that one :). But yea, the proper solution is what you have: to isolate this out to a service. But, to make it even simpler, I usually create my own base controller and add some shortcut method there, so that I can do something like $errors = $this->getJsonErrors($form). A service is really the best way to go, and together with that shortcut, it's got the "ease" of putting it on the form class (hopefully).

    Cheers!

  • 2017-10-03 Robert

    Hi Ryan,

    Thanks for such quick feedback. I though that might not be the right place.

    So I am trying to create convinient error wrapper. When you call $myForm->getErrors() you have to make couple of loops and a little bit of additional work to get errors. I would like to be able to call $myForm->getJsonErrors() and couple simillar methods. This would be my custom functionality shared accros all the forms.

    I would like to know if I can do this "From within the form" or I have to use it as separate service and then pass data to that service? I have this second solution working but I am really keen to make first one work.

    Can I or should I override main form class?

    Cheers!

  • 2017-10-03 weaverryan

    Yo Robert!

    Yea... as you discovered, you cannot do this. The problem is that your "type" class is kind of a "recipe". And when you call createForm(), the form component reads your recipe and creates a Form object from it. At that point, your *Type is discarded. The correct solution is to put this method somewhere else... and the right spot depends on what you want to do - common options are a private method in your controller, method in your entity or a service class. If you want a suggestion, let me know what the method does and I'll at least tell you where I would put it :).

    Cheers!

  • 2017-10-03 Robert

    Hello I have one question here. Can I add and how can I add custom function to form in customType class.

    My goal: I would like to be able to do something like

    $myForm->myCustomFunction()

    I have tried to add it directly in customType class but when I try to use it in controller like I did above I get not existing method name error. Is that possible?

    Regards,
    Rob