So I've spent around 5 or 6 hours today battling with Symfony2 forms and am at the point where I'd like some advice from other members of the community. I've tried over 3 different methods to achieve what I'm after and had no success. I've read through the docs, googled everything, asked others, and I'm only a little bit better off than when I started.
I'm building a system where you can order tickets. But the core problem is how to design the order part of the system.
After reading around and trying different things, I gathered that to represent the Order's Ticket and quantity, I needed another entity OrderTicket corresponds to an OrderItem from https://github.com/beberlei/AcmePizzaBundle and the Pizza is my Ticket.
On my order page where an order is created, I want the following:
The Order/OrderTicket/Ticket design is largely based on https://github.com/beberlei/AcmePizzaBundle
/**
* @ORM\Entity(repositoryClass="Foo\BackendBundle\Entity\TicketsRepository")
* @ORM\HasLifecycleCallbacks
* @ORM\Table(name="tickets")
*/
class Tickets
{
// id fields and others
/**
* @Assert\NotBlank
* @ORM\Column(type="string", nullable=true)
*/
protected $name;
/**
* @ORM\Column(type="date", name="available_from", nullable=true)
*/
protected $availableFrom;
/**
* @ORM\Column(type="date", name="available_to", nullable=true)
*/
protected $availableTo;
}
/**
* @ORM\Table()
* @ORM\Entity
*/
class OrderTicket
{
// id field
/**
* @ORM\Column(name="quantity", type="integer")
*/
private $quantity;
/**
* @ORM\ManyToOne(targetEntity="Tickets")
*/
protected $ticket;
/**
* @ORM\ManyToOne(targetEntity="Orders", inversedBy="tickets")
*/
protected $order;
// getters and setters for quantity, ticket and order
}
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
* @ORM\Table(name="orders")
*/
class Orders
{
// id field and other stuff
/**
* @ORM\OneToMany(targetEntity="OrderTicket", mappedBy="order", cascade={"persist"})
**/
protected $tickets;
/**
* @ORM\ManyToOne(targetEntity="Customer", cascade={"persist"})
*/
protected $customer;
public function __construct()
{
$this->tickets = new \Doctrine\Common\Collections\ArrayCollection();
}
// getters, setters, add for Tickets and Customer
}
/**
* @ORM\Table()
* @ORM\Entity
*/
class Customer
{
// id, name, email, address fields
}
This creates a schema like so (table naming differences are from auto generation):
CREATE TABLE `tickets` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`available_from` date DEFAULT NULL,
`available_to` date DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `Customer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`address` longtext COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `OrderTicket` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ticket_id` int(11) DEFAULT NULL,
`order_id` int(11) DEFAULT NULL,
`quantity` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`customer_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
);
class CustomerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('name')
->add('address')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Foo\BackendBundle\Entity\Customer'
));
}
public function getName()
{
return 'foo_backendbundle_customertype';
}
}
class OrderTicketType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('quantity', 'integer')
->add('ticket')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Foo\BackendBundle\Entity\OrderTicket'
));
}
public function getName()
{
return 'foo_backendbundle_ordertickettype';
}
}
class OrdersType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('customer', new CustomerType())
->add('tickets', 'collection', array(
'type' => new OrderTicketType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Foo\BackendBundle\Entity\Orders',
));
}
public function getName()
{
return 'foo_backendbundle_orderstype';
}
}
<form action="{{ path('index') }}" method="post" {{ form_enctype(form) }}>
<h3>Tickets</h3>
{{ form_errors(form) }}
<table>
<thead>
<tr>
<td>Ticket</td>
<td>Quantity</td>
</thead>
<tbody>
{% for ticketrow in form.tickets %}
<tr>
<td>{{ form_widget(ticketrow.ticket) }}</td>
<td>{{ form_widget(ticketrow.quantity) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h3>Customer</h3>
{% for customer in form.customer %}
{{ form_row(customer) }}
{% endfor %}
</form>
class DefaultController extends Controller
{
/**
* @Route("/", name="index")
* @Template()
*/
public function indexAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
// IMPORTANT - the Tickets are prefiltered for active Tickets, these have to be injected into the Order atm. In other places I use this method on the query builder
$tickets = $em->getRepository('FooBackendBundle:Tickets')->findActive();
// check no tickets
$order = new Orders();
// To prepopulate the order with the available tickets, we have to do it like this, due to it being a collection,
// rather than using the forms query_builder like everywhere else
foreach($tickets as $ticket) {
$ot = new OrderTicket();
$ot->setTicket($ticket);
$ot->setQuantity(0);
$ot->setOrder($order);
$order->addTicket($ot);
}
$form = $this->createForm(new OrdersType(), $order);
if ($request->isMethod('POST')) {
$form->bind($request);
// IMPORTANT here I have to remove the previously added Tickets where the quantity is 0 - as they're not wanted in the Order. Is there a better way to do this?
// if the quantity of Ticket is 0, do not add to order
// note we use the validation callback in Order to check total quantity of OrderTickets is > 0
$order->removeTicketsWithNoQuantity();
if ($form->isValid()) {
$em->persist($order);
$em->flush();
return $this->redirect($this->generateUrl('order_show', array('id' => $order->getId())));
}
}
return array('form' => $form->createView());
}
}
This works and will save the Order correctly, but I'm not sure it is the correct way to do what I want, and it does not display as I want.
You can see below in the images how it looks and how the Order goes through. It is worth noting that in each of the Ticket drop downs is the rest of the Tickets but which are not active.
The Order page:
The Order summary page after save:
The 3 Tickets displayed are the ones that have been filtered, and I only want these Tickets on the form. I ONLY WANT TO SEE THE TICKET NAME, NOT AN EDITABLE DROP DOWN.
The core problem is that they are presented as editable drop downs. I may just want a text string of the Ticket name, or maybe even the Ticket price in the future. I'm not sure how to achieve this. I know that the ticket field and relationship must be rendered somehow so that it can be bound in the controller. So basically I want to be able to use the Ticket entity and it's fields on the same row as the quantity text box.
So let's step out of the crapstorm of Symfony2's forms and put this in perspective - in the normal world, obviously I'd just retrieve the Tickets, then for each Ticket, I'd print the Ticket name, any other stuff I wanted, a hidden Ticket id, then an input for the Ticket quantity. Back into SF2 a little - I guess I need the Ticket entity available whilst looping the OrderTicket collection.
Please help me!
The easiest solution based on your code above would be create a custom type for your Ticket entity that simply displays the current ticket as a label, and also create a data transformer for it.
namespace WineVision\BackendBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\FormBuilderInterface;
use WineVision\BackendBundle\Form\Transformer\TicketToIdTransformer;
class TicketLabelType extends AbstractType
{
private $om;
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new TicketToIdTransformer($this->om);
$builder->addViewTransformer($transformer);
}
public function getParent()
{
return 'hidden';
}
public function getName()
{
return 'ticket_label_type';
}
}
Then create a widget in Resources/Form/fields.html.twig
{% block ticket_label_type_widget %}
{% spaceless %}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} />
<span class="ticketName">{{ form.vars.data.ticketNameMethod }}</span>
{% endspaceless %}
{% endblock %}
TicketToIdTransformer:
namespace WineVision\BackendBundle\Form\Transformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Doctrine\Common\Persistence\ObjectManager;
class TicketToIdTransformer implements DataTransformerInterface
{
private $om;
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function transform($ticket)
{
if (null === $ticket) {
return "";
}
if (!$ticket instanceof \WineVision\BackendBundle\Entity\Ticket) {
throw new UnexpectedTypeException($ticket, '\WineVision\BackendBundle\Entity\Ticket');
}
return $ticket->getId();
}
public function reverseTransform($id)
{
if ('' === $id || null === $id) {
return null;
}
return $this->om
->getRepository('WineVisionBackendBundle:Ticket')
->findOneBy(array('id' => $id));
}
}
Then create the service for your TicketType and pass the doctrine.orm.entity_manager as an argument, and in your OrderTicketType, use
$builder->add('ticket', 'ticket_label_type');
That should solve your problem for the code you've given above. To further extend the solution, you shouldn't pre-populate each order with each ticket type, and instead create a a custom collectiontype that uses Form Events to populate the collection with all of the ticket fields.
Hope this helps! I apologize if there are any syntax errors here. I've copied the code from one of my applications, and modified it for your needs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With