我的用例
我正在建立一个可以订购门票的系统.但核心问题是如何设计系统的订单部分.
>票证有一个名称,以及它可用的开始和结束日期(其他东西也是如此,但让我们保持示例简单.
>订单可能会选择多个故障单,并且每个故障单都有一个数量.
>订单有客户.这部分很好,工作花花公子!
在阅读并尝试不同的事情后,我收集了代表订单的票和数量,我需要另一个实体OrderTicket对应于https://github.com/beberlei/AcmePizzaBundle的OrderItem,而Pizza是我的票.
> OrderTicket有票和数量.
>客户详细信息表 – 姓名,电子邮件,地址.这部分工作正常.
>门票的表格.我希望在文本框或字符串中显示故障单名称;不在选择框中(现在正在发生的事情).我希望在故障单名称旁边指定数量.如果没有设置数量,则表示未选择故障单.
>根据今天的日期,门票应该在可用的位置进行过滤 – 这可以通过在带有查询构建器闭包的表单类型上使用自定义存储库方法在其他地方(在创建它们的后端管理中)实现.
我的后端
Order / OrderTicket / Ticket设计主要基于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; }
OrderTicket
/** * @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 }
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()); } }
这可以正常保存订单,但我不确定这是正确的方式来做我想要的,它不会显示我想要的.
您可以在下面的图像中看到它的外观和顺序如何通过.值得注意的是,在每张Ticket下拉列表中,其余的门票都是活跃的.
订单页面:
显示的3个门票是已过滤的门票,我只想在窗体上使用这些门票.我只想看到门票名称,而不是可编辑的下降.
核心问题是它们被呈现为可编辑的下拉.我可能只想要一个Ticket名称的文本字符串,或者甚至可能是将来的Ticket价格.我不知道如何实现这一目标.我知道票证字段和关系必须以某种方式呈现,以便它可以绑定在控制器中.所以基本上我希望能够使用Ticket实体及其与数量文本框在同一行的字段.
所以,让我们走出Symfony2表格的喧嚣,并把它放在正常的视野中 – 在正常的世界中,显然我只是检索票证,然后对于每个票证,我打印票证名称,我想要的任何其他东西,隐藏的票证ID,然后是票证数量的输入.回到SF2一点 – 我想在循环OrderTicket集合时我需要Ticket实体.
请帮我!
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'; } }
然后在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)); } }
然后为TicketType创建服务并将doctrine.orm.entity_manager作为参数传递,并在OrderTicketType中使用
$builder->add('ticket','ticket_label_type');
这应该可以解决您上面给出的代码的问题.要进一步扩展解决方案,您不应使用每种故障单类型预先填充每个订单,而是创建一个自定义集合类型,该类型使用表单事件来使用所有故障单字段填充集合.