Creating New Doctrine Entities Using Symfony's Form


In this video we are going to get hands-on experience in creating new Doctrine entities using the Symfony form.

We saw in the previous series on the basics of using Doctrine how we could create entities programmatically. This is a very useful skill to have. However in the vast majority of projects you are likely to want to manage / edit / update your data. And the easiest way to do that is to create a form which allows you to administer your data via a web page.

Whilst in this video we will learn how to create new entities using the Symfony, it is useful to know that in the next video we will learn how to edit existing data / entities also using the Symfony form.

Knowing this, we can make an educated guess that the form we use to create entities is going to look extremely similar to the form we use to edit existing data / entities. Whilst in the previous video we built our form using the Form Builder directly inside our controller action, we don't want to have to keep building the same form, duplicating that logic many times.

Now, we could simply extract the form builder code out from our first controller action into a private method, and then call that private method inside any other methods in the same controller / class that need that form.

But what if we want to use that form from outside the current controller?

Well, fortunately the Symfony framework has us covered. The solution is to create a standalone form class.

In Symfony-land we have a funny name for standalone forms - we put them in a class with the name of our form, suffixed with Type. Therefore, we might have a ContactType for our contact form. Or a ProfileType for managing user profiles. Or a PasswordResetType for allowing a user to reset his or her password.

I have never found out why we use the word Type in the context of Symfony's form, so if you know, do please leave a comment to tell me why.

Each standalone form class you create will have an extremely similar 'shape'. Personally I really like this, as it means forms start to look and feel the same. Once you have become comfortable with how one standalone form class works, you can immediately transfer that knowledge not just to any other form in your project, but in any other form in any other project you work on.

Generally, forms look like this:

// src/AppBundle/Form/Type/YourFormType.php

namespace AppBundle\Form\Type;

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\SubmitType;

class YourFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('someField', TextType::class)
            ->add('save', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\YourEntity',
        ));
    }
}

When working with entity-backed forms, it is important to match up the name (the first parameter in the add method) with the name of the property on your entity. This doesn't need to be the case in more advanced circumstances, but for the most part you will keep these two aligned.

In this example above, you would expect YourEntity to have a property called someField.

On our entity (see below for the Product entity in use in this video) we have four properties:

  • id
  • title
  • available from
  • description

I don't want form users to be able to edit the ID, so I'm going to leave that field out of my form definition. Without a setter method on my entity for setId this wouldn't work anyway.

Text inputs, and text areas are common form field types in use across almost every form we make. These map up nicely to our Doctrine entity properties:

// src/AppBundle/Entity/Product.php

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;

These map really nicely into our form:

// src/AppBundle/Form/Type/ProductType.php
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            ->add('description', TextareaType::class)
        ;
    }

The more interesting field of the three in this example is the DateTimeType, which maps to the $availableFrom property:

// src/AppBundle/Entity/Product.php

    /**
     * @ORM\Column(type="datetime")
     */
    protected $availableFrom;

// and on the form:

// src/AppBundle/Form/Type/ProductType.php

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('availableFrom', DateTimeType::class)
        ;
    }

At first glance this looks almost identical to the previous two fields. And that's awesome, because everything follows this common convention.

It's only when we go to render the form that things become clearly different. By default, Symfony will render out a set of dropdowns / select inputs, making it easy for form users to select a date and time from the available, correctly configured lists. This is such a time saver, and a real improvement for so little amount of code. Of course, you can customise this as you see fit, including reverting it back to a standard text input so you can whack some funky JavaScript date picker of your choice over the top as needed, but it's cool it's available for us right out of the box.

Because we are working with entities, we also need to tell the form which entity this configuration matches up too. Doing this is really simple, and again, something that you will see frequently in most Symfony form types:

// src/AppBundle/Form/Type/ProductType.php

use Symfony\Component\OptionsResolver\OptionsResolver;

// * snip *

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\Product'
        ]);
    }

All this is doing is telling Symfony what the underlying class is that is in use by this form. We can set other options inside the configureOptions method, but that is a little more advanced and we don't need that functionality just now.

What is interesting here is that if we do not define the data_class option, we can still use our form just fine - we just need to be explicit about the entity in use by the form. This is more obvious with a quick example:

If we don't define the data_class default option, we can pass in the entity during form creation, as the second parameter:

use AppBundle\Entity\Product;

// * snip *

    public function formExampleAction()
    {
        $form = $this->createForm(ProductType::class, new Product());

Symfony is clever enough to figure out that we would need a new Product() entity if we define the data_class, but don't pass in a new Product():

// in our form type
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\Product'
        ]);
    }

// and in our controller action

    public function formExampleAction()
    {
        $form = $this->createForm(ProductType::class);

In my opinion, your code is more readable for your future self / other developers if you are explicit about declaring the new Product() when creating the form in your controller, regardless of whether you explicitly declare your data_class. In my opinion, you should do both - set the right data_class, and pass in the new entity.

From the Controller, because we are no longer building the form right inside the controller, the syntax to use this form type is much simplified:

use AppBundle\Form\Type\ProductType;
use Symfony\Component\HttpFoundation\Request;

// * snip *

    public function formExampleAction(Request $request)
    {
        $form = $this->createForm(ProductType::class);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            // things went well

            return $this->redirectToRoute('form_example');
        }

        return $this->render(':form-example:index.html.twig', [
            'productForm' => $form->createView()
        ]);
    }

The real difference here is in the first line in our controller action, but if the rest of this is at all unfamiliar to you then be sure to check out the previous video.

We've gone from an inline, multiline form definition in the previous video to a simple, single line : $form = $this->createForm(ProductType::class);

And what's even nicer about this is we can re-use this over and over, in any controller action that needs to access this particular form. For me, this is a total win.

However, we aren't done just yet. After all, we aren't properly handling our form submission.

Once some data is submitted, how can we go about converting that submitted form data into an entity that Doctrine can save / persist?

Good news! It is amazingly easy:

if ($form->isSubmitted() && $form->isValid()) {

    $em = $this->getDoctrine()->getManager();

    $product = $form->getData();

    $em->persist($product);
    $em->flush();

    return $this->redirectToRoute('form_example');
}

We covered the form checks in the previous video so be sure to review that video if you aren't sure what's happening on line 1.

Then we get the entity manager. Well, again, by this point we already know about how to get the entity manager, and if you don't then please watch this series now.

Once we have the entity manager, well... we need an entity to manage!

$product = $form->getData();

We just got given a pre-configured Product entity without having to call new Product, or map any of the data by calling setTitle, setAvailableAt, etc.

Amazing, right?

We already told the form (by way of configureOptions) that the underlying data for this form will be an entity of type AppBundle\Entity\Product.

Symfony's form component is therefore capable of taking our user's submitted data, mapping it onto a new entity, validating that the submitted data is acceptable (we haven't done validation here, but it is worth mentioning) and boom, in one method we have a pre-configured, usable entity.

All that's left to do at this stage is to persist it and flush that new data off to our database. Again, if unsure on these terms then please watch the Doctrine for Beginners series now.

Lastly we just redirect back to the form, clearing all the submitted fields by way of a full page reload.

For me, this is a really simple and easy to use system. What makes this even more powerful is how we can - with but a few minor tweaks - re-use all of this to start editing existing Doctrine entities. Which is what we shall do, in the very next video.

Reference Code From This Video

Form Example Controller:

<?php

namespace AppBundle\Controller;

use AppBundle\Entity\Product;
use AppBundle\Form\Type\ProductType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class FormExampleController extends Controller
{
    /**
     * @Route("/", name="form_example")
     */
    public function formExampleAction(Request $request)
    {
        $form = $this->createForm(ProductType::class);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            $em = $this->getDoctrine()->getManager();

            $product = $form->getData();

            $em->persist($product);
            $em->flush();

            return $this->redirectToRoute('form_example');
        }

        return $this->render(':form-example:index.html.twig', [
            'productForm' => $form->createView()
        ]);
    }
}

Product Type:

<?php

// src/AppBundle/Form/Type/ProductType.php

namespace AppBundle\Form\Type;

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

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            ->add('availableFrom', DateTimeType::class)
            ->add('description', TextareaType::class)
            ->add('save', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\Product'
        ]);
    }
}

Product entity:

<?php

// src/AppBundle/Entity/Product.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="products")
 */
class Product
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

    /**
     * @ORM\Column(type="datetime")
     */
    protected $availableFrom;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return mixed
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param mixed $title
     * @return Product
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getAvailableFrom()
    {
        return $this->availableFrom;
    }

    /**
     * @param mixed $availableFrom
     * @return Product
     */
    public function setAvailableFrom($availableFrom)
    {
        $this->availableFrom = $availableFrom;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @param mixed $description
     * @return Product
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }
}

Code For This Course

Get the code for this course.

Episodes