symfonysymfony-2.5

Add additional items to a Symfony2 Form Collection type after page submit


I have a form built on the idea of a Purchase. It has Purchase Items, a Payment Amount and a Payment Type (VISA, Mastercard, Cash). I have the form preloaded with 2 Purchase Items however I am trying to add an additional Purchase Item if the user chooses a Card Payment Type (VISA or Mastercard) Type. This additional Purchase Item I am trying to add via the Controller.

Rendered Form view

The question is really, where do I implement this functionality in the Controller Action... Or is it better as an EventListener on the Form Type?

When the form is submitted with the additional Card Fee Purchase Item I get the following error...

Catchable Fatal Error: Argument 1 passed to Aazp\BookingBundle\Entity\PurchaseItem::__construct() must be an instance of Aazp\BookingBundle\Entity\Product, none given, called in /project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/FormType.php on line 141 and defined in /project/src/Aazp/BookingBundle/Entity/PurchaseItem.php line 20

The BookingController

namespace Aazp\BookingBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Aazp\BookingBundle\Entity\Booking;
use Aazp\BookingBundle\Entity\Passenger;
use Aazp\BookingBundle\Entity\Payment;
use Aazp\BookingBundle\Entity\Purchase;
use Aazp\BookingBundle\Entity\PurchaseItem;

use Aazp\BookingBundle\Form\PurchaseItemType;
use Aazp\BookingBundle\Form\PurchaseType;

use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class BookingController extends Controller
{

    public function passengerPaymentAction($passenger_id)
    {
        $request = Request::createFromGlobals();
        $purchaseBalance = 0.0;

        //Query the selected Passenger
        $em = $this->getDoctrine()->getManager();
        $passenger = $request->attributes->get('passenger', $em->getRepository('AazpBookingBundle:Passenger')->find($passenger_id));
        if (!$passenger) {
            throw $this->createNotFoundException('Unable to find Passenger entity.');
        }

        $purchase = $passenger->getPurchase();
        //Has this Passenger made a Payment. If yes then Purchase exists.
        if($purchase === NULL) //If Purchase does not exist then preload the form with default products.
        {
            $purchase = new Purchase();

            $product_category_photo = $em->getRepository('AazpBookingBundle:ProductCategory')->findOneByName('FLIGHT-PHOTO');

            $product_photo_option = $em->getRepository('AazpBookingBundle:Product')->findOneByProductCategory($product_category_photo);
            if (!$product_photo_option) {
                throw $this->createNotFoundException('Unable to find Flight Photo Product entity.');
            }

            $purchase_item_flight = new PurchaseItem($passenger->getFlight());
            $purchase_item_photo_option = new PurchaseItem($product_photo_option);

            //Add Purchase Items to the Purchase
            $purchase->addPurchaseItem($purchase_item_flight);
            $purchase->addPurchaseItem($purchase_item_photo_option);

            //Set the Purchase on the Passenger
            $passenger->setPurchase($purchase);
        }
        //Ajax call triggered by onChange event on PaymentType radio button in form
        //Add additional Purchase Item for Card Type Payment
        if($form->get('paymentType')->getData()->getId() > 1)
        {
            //PaymentType selected/modified then calculate Payment Fee
            $product_category_card_fee = $em->getRepository('AazpBookingBundle:ProductCategory')->findOneByName('CARD-FEE');
            $product_card_fee = $em->getRepository('AazpBookingBundle:Product')->findOneByProductCategory($product_category_card_fee);
            if (!$productcard_fee) {
            throw $this->createNotFoundException('Unable to find Card Fee Product entity.');
            }

            $purchase_item_card_fee = new PurchaseItem($product_card_fee);

            //Add Purchase Items to the Purchase
            $purchase->addPurchaseItem($purchase_item_card_fee);

            $passenger->setPurchase($purchase);

            return $this->render('AazpBookingBundle:Purchase:summary.html.twig', array(
                'passenger' => $passenger,
                'form'  => $form->createView(),
            ));
        }

        $form = $this->createForm(new PurchaseType($em), $purchase);

        $form->handleRequest($request);

        //If form is Valid create Payment and persist.
        if ($form->isValid())
        {
            $payment = new Payment();
            $payment->setAmount($form->get('paymentAmount')->getData());
            $payment->setPaymentType($form->get('paymentType')->getData());
            $payment->setDescription($form->get('description')->getData());

            $passenger->getPurchase()->addPayment($payment);
            $passenger->getBooking()->setStatus(Booking::STATUS_CONFIRMED);

            $em->persist($passenger->getPurchase());
            $em->flush();
            $this->get('session')->getFlashBag()->add('message', 'Payment '.$payment->getAmount().' CHF has been successful!');

            return $this->redirect($this->generateUrl('booking_show', array ('id'=> $passenger->getBooking()->getId())));
        }

        return $this->render('AazpBookingBundle:Purchase:summary.html.twig', array(
            'passenger' => $passenger,
            'form'  => $form->createView(),
        ));
    }
}

The PurchaseItemType

namespace Aazp\BookingBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Aazp\BookingBundle\Entity\ProductRepository;

class PurchaseItemType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('product', 'entity', array('label' => 'Flight', 'class' => 'AazpBookingBundle:Product','property' => 'name', 'empty_value' => 'Please Select', 'required' => false, ));
        $builder->add('amount', 'number', array('precision' => 2));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array('data_class' => 'Aazp\BookingBundle\Entity\PurchaseItem', ));
    }

    public function getName()
    {
        return 'purchaseItem';
    }
}

The PurchaseType

namespace Aazp\BookingBundle\Form;

use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

use Aazp\BookingBundle\Entity\PurchaseItem;

class PurchaseType extends AbstractType
{
    protected $em;

    function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('purchaseItems', 'collection', array('type' => new PurchaseItemType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false));
        $builder->add('paymentType', 'entity', array('label' => 'Payment Type', 'class' => 'AazpBookingBundle:PaymentType','property' => 'name', 'mapped' => false, 'expanded' => true));
        $builder->add('paymentAmount', 'number', array('precision' => 2, 'data' => 0.0, 'mapped' => false));
        $builder->add('description', 'text', array('mapped' => false, 'required' => false));
        $builder->add('cancel', 'submit', array('attr' => array('formnovalidate' => true, 'data-toggle' => 'modal', 'data-target' => '#cancelWarning', )));
    $builder->add('pay', 'submit');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array('data_class' => 'Aazp\BookingBundle\Entity\Purchase', ));
    }

    public function getName()
    {
        return 'purchase';
    }
}

The Purchase Entity

<?php
namespace Aazp\BookingBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;

use Aazp\MainBundle\Entity\BaseEntity;

/**
 * @ORM\Entity(repositoryClass="Aazp\BookingBundle\Entity\PurchaseRepository")
 * @ORM\Table(name="purchase")
 * @Gedmo\SoftDeleteable(fieldName="deleted")
 */
class Purchase extends BaseEntity
{
    /**
     * Constructor
     */
    public function __construct()
    {
        $this->purchaseItems = new \Doctrine\Common\Collections\ArrayCollection();
        $this->payments = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToMany(targetEntity="PurchaseItem", cascade={"all"})
     * @ORM\JoinTable(name="purchase_purchase_items",
     *      joinColumns={@ORM\JoinColumn(name="purchase_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="purchase_item_id", referencedColumnName="id", unique=true)}
     *      )
     **/
    protected $purchaseItems;

    /**
     * @ORM\ManyToMany(targetEntity="Payment", inversedBy="purchases", cascade={"all"})
     * @ORM\JoinTable(name="purchases_payments",
     *      joinColumns={@ORM\JoinColumn(name="purchase_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="payment_id", referencedColumnName="id")}
     *      )
     **/
    protected $payments;

    /**
     * @ORM\OneToOne(targetEntity="Passenger", mappedBy="purchase")
     **/
    protected $passenger;


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

    /**
     * Add purchaseItems
     *
     * @param \Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems
     * @return Purchase
     */
    public function addPurchaseItem(\Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems)
    {
        $this->purchaseItems[] = $purchaseItems;

        return $this;
    }

    /**
     * Remove purchaseItems
     *
     * @param \Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems
     */
    public function removePurchaseItem(\Aazp\BookingBundle\Entity\PurchaseItem $purchaseItems)
    {
        $this->purchaseItems->removeElement($purchaseItems);
    }

    /**
     * Get purchaseItems
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getPurchaseItems()
    {
        return $this->purchaseItems;
    }

    /**
     * Add payments
     *
     * @param \Aazp\BookingBundle\Entity\Payment $payments
     * @return Purchase
     */
    public function addPayment(\Aazp\BookingBundle\Entity\Payment $payments)
    {
        $this->payments[] = $payments;

        return $this;
    }

    /**
     * Remove payments
     *
     * @param \Aazp\BookingBundle\Entity\Payment $payments
     */
    public function removePayment(\Aazp\BookingBundle\Entity\Payment $payments)
    {
        $this->payments->removeElement($payments);
    }

    /**
     * Get payments
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getPayments()
    {
        return $this->payments;
    }

    /**
     * Set passenger
     *
     * @param \Aazp\BookingBundle\Entity\Passenger $passenger
     * @return Purchase
     */
    public function setPassenger(\Aazp\BookingBundle\Entity\Passenger $passenger = null)
    {
        $this->passenger = $passenger;

        return $this;
    }

    /**
     * Get passenger
     *
     * @return \Aazp\BookingBundle\Entity\Passenger 
     */
    public function getPassenger()
    {
        return $this->passenger;
    }   
}

Solution

  • This happened because the default data_class option calls that constructor with no arguments. If your class Aazp\Booking Bundle\Entity\PurchaseItem has parameters in the constructor, you need to use the empty_data option to instantiate:

    // Aazp\BookingBundle\Form\PurchaseItemType.php
    
    $resolver->setDefaults([
        'empty_data' => function (FormInterface $form) {
            return new PurchaseItem($form->get('product')->getData());
        },
    ]);
    

    You can lean more about this option here.