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.