PizzaBundle Hacking Day

Here we are going to disect what is going on with beberlei’s PizzaBundle. The reason is that I have ran into the following problem.

The problem at hand is that we don’t know how to render a form where there is a relationship of OneToMany to a collection but where we want to display fields for only one new entity each time we persist.

So we start by installing PizzaBundle in an effort to dissect the app:

So the form I found to be rather *simple* on pizzabundle. It is true that it has some nice javascript and all but i cannot for instance customize the inners of the pizza. My problem involves customizing exactly one pizza, but the inner parts say price and type/name then that gets added or persisted so my problem is somewhat similar but a little bit extra is needed so I guess i am going to have to hack it like they do.

So let’s look on the index action for the controller that sets the main start form for entering the orders on PizzaBundle. Everything seems normal until you see that what is being passed as second argument to the createForm method is not an entity as we normally but a form factory which is created for the order form type passed as well. In addition what is persisted when the form is valid is the entity which is product of the $factory->make():

 public function indexAction()
    {
        $em = $this->getDoctrine()->getEntityManager();
 
        $factory = new OrderFactory($em);
 
        $form = $this->createForm(new OrderFormType(), $factory);
 
        $request = $this->getRequest();
 
        if ('POST' === $request->getMethod()) {
 
            $form->bindRequest($request);
 
            if ($form->isValid()) {
 
                $em->persist($order = $factory->make());
                $em->flush();
 
                $this->get('session')->setFlash('success', 'New order were saved!');
 
                return $this->redirect($this->generateUrl('acme_pizza_order_show', array(
                    'id' => $order->getId(),
                )));
            }
        }
 
        return array('form' => $form->createView());
    }

Now let’s look more in depth what is inside the OrderFormType. Basically we see that the builder adds elements not necessarily associated with an entity in particular but rather this is the form model part (first part) that assigns the fields that the form will render:

 public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('known_customer', 'checkbox', array(
                'required' => false,
            ))
            ->add('known_phone', 'text')
            ->add('customer', new Type\CustomerType())
            ->add('items', 'collection', array(
                'type'         => new Type\OrderItemType(),
                'allow_add'    => true,
                'allow_delete' => true,
                'prototype'    => true,
            ))
        ;
    }

The second part of the form model corresponds to what is being implemented by the $factory. This $factory part is the one in charge of not assigning the fields of the form to be rendered but rather talk of how the form fields relate to what is being persisted in terms of entities as the $em->persist() call only understands ORM object persistence lingo.

The $factory definition is located under Entity folder and under a Factory folder. Not sure if this is a standard practice, however what I do think is that the name should rather be called FormModel, as it deals with the Form component but it is not precisely an entity.

The factory name is justified in a sense because it creates or builds the entity inside the method make of the factory passed.

/**
 * @return \Acme\PizzaBundle\Entity\Order
 */
public function make()
{
    $order = new Order();
    $order->setCustomer($this->customer);
 
    foreach ($this->items as $item) {
        $order->addItem($item);
    }
 
    return $order;
}

As we can see here the object Order is prepared built and shipped out with the elements that are received by the form, that is: customer and $items.

In addition to this there is some kind of custom validation that is performed by the form model class or factory class. The validation is indicated mainly over annotations:

/**
 * @Assert\callback(methods={"isValidCustomer", "pickedOrderItems"})
 */

The methods then are implemented respectively and use the ExecutionContext class $context to return violations or simply just return true or false.

At this point it is good remembering also that the definition for createForm is really:

return $this->container->get('form.factory')->create($type, $data, $options);

And that $type and $data are very independent on any persistence we can handle outside of this service.

Finally I would like to end this article posing the limitations the the current approach has. It is not very DRY because it tries to reinvent the wheel parts already implemented in the forms component. For overcoming this limitation please see the other blog post on Extending Forms.

If you have found this article helpful please donate with the link above. Thanks!

4 thoughts on “PizzaBundle Hacking Day

  1. Thanks for presenting this demo bundle. I´m doing some test projects with Symfony2 at the moment and I really needed an example for the forms usage.

  2. Pingback: Symfony 2.0: Embedded Forms for Collections | Scott Sherwood

Leave a Reply to Luiz Cancel reply

Your email address will not be published. Required fields are marked *