Symfony Forms

·

At the end of several years working with Symfony, I’m taking a moment to appreciate its strongest points. In particular, allowing users to apply mutations to objects via HTML forms.

The output of a Symfony endpoint (controller action) is usually either a data view (displaying data as HTML, rendered with Twig), or a form view (displaying a form as HTML and receiving the submitted form).

Symfony isn’t particularly RESTful, though you can use collection + resource-style URLs if you like:

  • /articles/ - GET the collection of articles
  • /articles/_create - GET/POST a form to create a new article
  • /articles/{id}/ - GET the article
  • /articles/{id}/_edit - GET/POST a form to edit the article
  • /articles/{id}/_delete - GET/POST a form to delete the article

The entity, controller, form and voter for creating and editing an article look like something this:


// ArticleBundle/Entity/Article.php

class Article
{
    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     * @Assert\NotBlank
     */
    private $title;

    /**
     * @var string
     *
     * @ORM\Column(type="text")
     * @Assert\NotBlank
     * @Assert\Length(min=100)
     */
    private $description;

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

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

    /**
     * @param string $title
     */
    public function setTitle($title)
    {
        $this->title = $title;
    }

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

    /**
     * @param string $description
     */
    public function setDescription($description)
    {
        $this->description = $description;
    }
}


// ArticleBundle/Controller/ArticleController.php

class ArticleController extends Controller
{
    /**
     * @Route("/articles/_create", name="create_article")
     * @Method({"GET", "POST"})
     *
     * @param Request $request
     *
     * @return Response
     */
    public function createArticleAction(Request $request)
    {
        $article = new Article();
        $this->denyAccessUnlessGranted(ArticleVoter::CREATE, $article);

        $article->setOwner($this->getUser());

        $form = $this->createForm(ArticleType::class, $article);
        $form->handleRequest($request);

        if ($form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($article);
            $entityManager->flush();

            $this->addFlash('success', 'Article created');

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

        return $this->render('ArticleBundle/Article/create.html.twig', [
            'form' => $form->createView()
        ]);
    }

    /**
     * @Route("/articles/{id}/_edit", name="edit_article")
     * @Method({"GET", "POST"})
     *
     * @param Request $request
     * @param Article $article
     *
     * @return Response
     */
    public function editArticleAction(Request $request, Article article)
    {
        $this->denyAccessUnlessGranted(ArticleVoter::EDIT, $article);

        $form = $this->createForm(ArticleType::class, $article);
        $form->handleRequest($request);

        if ($form->isValid()) {
            $this->getDoctrine()->getManager()->flush();

            $this->addFlash('success', 'Article updated');

            return $this->redirectToRoute('articles', [
                'id' => $article->getId()
            ]);
        }

        return $this->render('ArticleBundle/Article/edit', [
            'form' => $form->createView()
        ]);
    }
}

// ArticleBundle/Form/ArticleType.php

class ArticleType extends AbstractType
{

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('title');

        $builder->add('description', null, [
            'attr' => ['rows' => 10]
        ]);

        $builder->add('save', SubmitType::class, [
            'attr' => ['class' => 'btn btn-primary']
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Article::class,
        ]);
    }
}

// ArticleBundle/Security/ArticleVoter.php

class ArticleVoter extends Voter
{
    const CREATE = 'CREATE';
    const EDIT = 'EDIT';

    public function vote($attribute, $article, TokenInterface $token)
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        switch ($attribute) {
            case self::CREATE:
                if ($this->decisionManager->decide($token, array('ROLE_AUTHOR'))) {
                    return true;
                }

                return false;

            case self::EDIT:
                if ($user === $article->getOwner()) {
                    return true;
                }

                return false;
        }
    }
}

// ArticleBundle/Resources/views/Article/create.html.twig

{{ form(form) }}

// ArticleBundle/Resources/views/Article/edit.html.twig

{{ form(form) }}

The combination of Symfony’s Form, Voter and ParamConverter allows you to define who (Voter) can update which properties (Form) of a resource, and when.

The Doctrine annotations allow you to define validations for each property, which are used in both client-side and server-side form validation.