Symfony2表单和集合 - Order,OrderItem实现

jmo*_*moz 5 php symfony

因此,我今天花了大约5或6个小时与Symfony2表格进行斗争,并且我希望得到社区其他成员的一些建议.我已经尝试了3种不同的方法来实现我所追求的并且没有成功.我已经阅读了文档,谷歌搜索了所有内容,问了其他人,而且比起初时我只有一点点好转.

我的用例

我正在建立一个可以订购门票的系统.但核心问题是如何设计系统的订单部分.

  • 都有一个名字,并开始和结束日期在何处使用(其他的东西很好,但让保持示例简单.
  • 一个订单可以有多个选定门票和每张机票有一个量.
  • 订单有客户. 这部分很好,工作花花公子!

在阅读并尝试不同的事情后,我收集了代表订单的票和数量,我需要另一个实体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;
}
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

订购

/**
 * @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
}
Run Code Online (Sandbox Code Playgroud)

顾客

/**
 * @ORM\Table()
 * @ORM\Entity
 */
class Customer
{
    // id, name, email, address fields

}
Run Code Online (Sandbox Code Playgroud)

这会创建一个类似的模式(表命名差异来自自动生成):

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`)
);
Run Code Online (Sandbox Code Playgroud)

形式

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';
    }
}
Run Code Online (Sandbox Code Playgroud)

表格

<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>
Run Code Online (Sandbox Code Playgroud)

最后是控制器

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());
    }
}
Run Code Online (Sandbox Code Playgroud)

摘要

这可以正常保存订单,但我不确定这是正确的方式来做我想要的,它不会显示我想要的.

您可以在下面的图像中看到它的外观和顺序如何通过.值得注意的是,在每张Ticket下拉列表中,其余的门票都是活跃的.

订单页面:

order1

保存后的订单摘要页面:

order2

显示的3个门票是已过滤的门票,我只想在窗体上使用这些门票. 我只想看到门票名称,而不是可编辑的下降.

核心问题是它们被呈现为可编辑的下拉.我可能只想要一个Ticket名称的文本字符串,或者甚至可能是将来的Ticket价格.我不知道如何实现这一目标.我知道票证字段和关系必须以某种方式呈现,以便它可以绑定在控制器中.所以基本上我希望能够使用Ticket实体及其与数量文本框在同一行的字段.

所以,让我们走出Symfony2表格的喧嚣,并把它放在正常的视野中 - 在正常的世界中,显然我只是检索票证,然后对于每个票证,我打印票证名称,我想要的任何其他东西,隐藏的票证ID,然后是票证数量的输入.回到SF2一点 - 我想在循环OrderTicket集合时我需要Ticket实体.

请帮我!

Mik*_*ike 3

基于上面的代码的最简单的解决方案是为您的票证实体创建一个自定义类型,该实体仅将当前票证显示为标签,并为其创建一个数据转换器。

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';
    }    
}
Run Code Online (Sandbox Code Playgroud)

然后在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 %}
Run Code Online (Sandbox Code Playgroud)

票证到 Id 转换器:

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));

    }
}
Run Code Online (Sandbox Code Playgroud)

然后为您的 TicketType 创建服务并将doctrine.orm.entity_manager 作为参数传递,并在您的 OrderTicketType 中使用

$builder->add('ticket', 'ticket_label_type');
Run Code Online (Sandbox Code Playgroud)

这应该可以解决您上面给出的代码的问题。为了进一步扩展解决方案,您不应使用每种票证类型预先填充每个订单,而应创建一个自定义集合类型,该集合类型使用表单事件来使用所有票证字段填充集合。

希望这可以帮助!如果这里有任何语法错误,我深表歉意。我从我的一个应用程序复制了代码,并根据您的需要进行了修改。