5

今日、私は Symfony2 フォームとの戦いに約 5 時間か 6 時間を費やしましたが、コミュニティの他のメンバーからのアドバイスが欲しいところです。私は、私が求めているものを達成するために3つの異なる方法を試しましたが、成功しませんでした. 私はドキュメントを読み、すべてをグーグルで検索し、他の人に尋ねましたが、始めたときよりも少しだけ良くなりました.

私のユースケース

チケットを注文できるシステムを構築中です。しかし、中心的な問題は、システムの注文部分をどのように設計するかです。

  • チケットには、名前と、それが利用可能な開始日と終了日があります (他のものも同様ですが、例を単純に保ちます。
  • 注文には複数のチケットが選択されている場合があり、各チケットには数量があります。
  • Order にはCustomerがあります。 この部分は問題なく、うまく機能します !

さまざまなことを試してみた後、注文のチケットと数量を表すために、 https://github.com/beberlei/AcmePizzaBundleの OrderItem に対応する別のエンティティOrderTicketが必要であり、Pizza が私のチケットであることがわかりました。

  • OrderTicketには Ticket と数量があります

注文が作成される注文ページで、次のものが必要です。

  • 顧客の詳細- 名前、電子メール、住所 のフォーム。この部分は正常に動作します
  • チケットのフォーム。チケット名をテキストボックスまたは文字列で表示したい。選択ボックスではありません(これは現在起こっていることです)。チケット名の横に数量を明記してほしい。数量が設定されていない場合は、チケットが選択されていないことを意味します。
  • チケットは、今日の日付に応じて利用可能な場所でフィルター処理する必要があります。これは、クエリ ビルダー クロージャーを使用してフォーム タイプでカスタム リポジトリ メソッドを使用することにより、別の場所 (チケットが作成されたバックエンド管理者) で実現されます。

私のバックエンド

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

オーダーチケット

/**
 * @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());
    }
}

概要

これは機能し、注文を正しく保存しますが、私が望むことを行う正しい方法であるかどうかはわかりません。また、希望どおりに表示されません

以下の画像で、それがどのように見え、注文がどのように行われるかを見ることができます. 各チケット ドロップダウンには、残りのチケットがありますが、アクティブではないことに注意してください。

注文ページ:

注文1

保存後の注文概要ページ:

注文2

表示されている 3 つのチケットはフィルター処理されたものであり、これらのチケットのみをフォームに表示したいと考えています。 編集可能なドロップダウンではなく、チケット名のみを表示したい

中心的な問題は、それらが編集可能なドロップダウンとして表示されることです。チケット名のテキスト文字列だけでなく、将来的にはチケット価格も必要になるかもしれません。これを達成する方法がわかりません。コントローラーでバインドできるように、チケット フィールドと関係を何らかの方法でレンダリングする必要があることはわかっています。したがって、基本的には Ticket エンティティを使用できるようにしたいと考えており、そのフィールドは数量テキスト ボックスと同じ行にあります。

それでは、Symfony2 のフォームのクラップストームから抜け出して、これを概観してみましょう。通常の世界では、明らかに、チケットを取得するだけで、各チケットに対して、チケット名やその他の必要なものを出力します。非表示のチケット ID、次にチケット数量の入力。SF2 に少し戻ります。OrderTicket コレクションをループしている間に、Ticket エンティティを利用できるようにする必要があると思います。

私を助けてください!

4

1 に答える 1

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';
    }    
}

次に、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');

これで、上記のコードの問題が解決するはずです。ソリューションをさらに拡張するには、各注文に各チケット タイプを事前入力するのではなく、フォーム イベントを使用してすべてのチケット フィールドをコレクションに入力するカスタム コレクション タイプを作成する必要があります。

お役に立てれば!ここに構文エラーがある場合はお詫び申し上げます。私のアプリケーションの 1 つからコードをコピーし、必要に応じて変更しました。

于 2013-01-09T19:01:06.863 に答える