The Form component is one of Symfony’s most powerful features. From adding fields and validating submitted data to more advanced features like form theming and form events, it has truly simplified what was one the most tedious development processes.
I’d be lying, however, if I said that Symfony forms have never been a source of frustration for me — but each time, I was happy to find that there was a solution that just hadn’t occurred to me. This is the story of one such situation.
If you’re bringing up a standard CRUD admin architecture — a list of entities with links and corresponding views for viewing, editing or deleting each one — everything’s pretty straightforward. But what happens when you get a mockup or design that calls for something a little more complicated?
Let’s say your app includes a lightweight entity that is backed by a simple reference table — a category, let’s say, that only has id
and name
fields. And let’s say that you know that only a handful will exist, so linking off to each edit view and possibly updating each one in turn seems like overkill. Why not consolidate your index and edit views and allow updating the entire collection of categories in a single form?
My thoughts were roughly: There’s no entity to which we can map a single form. Should we send a form for each Category instance to the view and make do with a handful of forms? There’s the CollectionType, but in this case there’s nothing to which we can attach the collection. Arrgh — how do we put all of this together?
Perhaps you’ve already spotted the pit into which I fell. I think it’s easy and understandable to associate forms only with entity classes when working with Symfony — after all, they hold the data you want to save. But forms can represent any business object, even transient ones that only need to exist for the duration of a single request. And that’s exactly what’s needed here — a CategoryCollection class whose only property is a collection of categories (natch).
namespace AppBundle\Model;
use Doctrine\Common\Collections\ArrayCollection;
class CategoryCollection
{
protected $categories;
public function __construct(array $categories)
{
$this->categories = new ArrayCollection($categories);
}
public function getCategories()
{
return $this->categories;
}
}
I’ve put this class inside the AppBundle\Model namespace specifically because it is transient and will not be persisted to a database. (There may be some confusion if you come from a Laravel background, in which entities are models and there’s not really a standard corollary for transient objects, though they can certainly be used.) In all other respects, this class resembles any other entity with a collection property. So we’re ready to build a form for it.
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\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class CategoryCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('categories', CollectionType::class, [
'entry_type' => CategoryType::class,
'entry_options' => [
'label' => false,
],
])
->add('Submit', SubmitType::class, ['attr' => ['class' => 'btn-primary']]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Model\CategoryCollection',
]);
}
}
And the form type for each individual Category is as follows.
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;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', TextType::class, [
'label' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Category',
]);
}
}
With these two form types in place, our controller method can be kept pretty lean. All we need to do is fetch the categories (all of them, in this case), wrap them in a CategoryCollection and pass the collection to its corresponding form:
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use AppBundle\Model\CategoryCollection;
use AppBundle\Form\Type\CategoryCollectionType;
class CategoryController extends Controller
{
/**
* @Route(path="/categories", methods={"GET", "POST"})
*/
public function indexAction(Request $request)
{
$em = $this->getDoctrine()
->getManager();
$categories = $em->getRepository('AppBundle:Category')
->findAll();
$categoryCollection = new CategoryCollection($categories);
$form = $this->createForm(CategoryCollectionType::class, $categoryCollection);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->flush();
$this->addFlash('notice', 'Categories were updated!');
return $this->redirect('/categories');
}
return $this->render('category/index.html.twig', ['form' => $form->createView()]);
}
}
And voilà! Now we can update all of our categories from a single form with practically no overhead simply by using a transient model as an intermediary between the underlying entities and the form.
One thing I elided here is that these categories already existed — adding or deleting categories using this form is certainly doable, but it requires a little more work. Because persistence will not cascade from the parent-level CategoryCollection
to its children, we would have to listen for additions and removals and call the appropriateEntityManager
methods individually. That could be the topic of a post in its own right, but I’ll at least add that it would involve configuring the CategoryCollection
form type as a service (so that we can easily pass in the EntityManager
) and listening to thePOST_SUBMIT form event
inside the form.
Posted in #Technologies under *Symfony