Подтвердить что ты не робот

Symfony2 обновляет элементы в "подформате"

Краткая версия моего вопроса:

Как редактировать объекты подформ в Symfony2?

= - = - = - = - = - = - = Длинная и подробная версия = - = - = - = - = - = - = - =

У меня есть сущность Order

<?php

class Order
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Customer")
     * @ORM\JoinColumn(name="customer_id", referencedColumnName="id", nullable=false)
     **/
    private $customer;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date", type="date")
     */
    private $date;

    /**
     * @ORM\ManyToOne(targetEntity="\AppBundle\Entity\OrderStatus")
     * @ORM\JoinColumn(name="order_status_id", referencedColumnName="id", nullable=false)
     **/
    private $orderStatus;

    /**
     * @var string
     *
     * @ORM\Column(name="reference", type="string", length=64)
     */
    private $reference;

    /**
     * @var string
     *
     * @ORM\Column(name="comments", type="text")
     */
    private $comments;

    /**
     * @var array
     *
     * @ORM\OneToMany(targetEntity="OrderRow", mappedBy="Order", cascade={"persist"})
     */
    private $orderRows;

    ...
}

MySQL

_____________________________________________________________
|id                           | order id                    |
|customer_id                  | fk customer.id NOT NULL     |
|date                         | order date                  |
|order_status_id              | fk order_status.id NOT NULL |
|reference                    | varchar order reference     |
|comments                     | text comments               |
|___________________________________________________________|

И объект OrderRow (заказ может иметь одну или несколько строк)

<?php

class OrderRow
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Order", inversedBy="orderRows", cascade={"persist"})
     * @ORM\JoinColumn(name="order_id, referencedColumnName="id", nullable=false)
     **/
    private $order;

    /**
     * @ORM\ManyToOne(targetEntity="[MyShop\Bundle\ProductBundle\Entity\Product")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id", nullable=true)
     **/
    private $product;

    /**
     * @var string
     *
     * @ORM\Column(name="description", type="string", length=255)
     */
    private $description;

    /**
     * @var integer
     *
     * @ORM\Column(name="count", type="integer")
     */
    private $count = 1;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date", type="date")
     */
    private $date;

    /**
     * @var decimal
     *
     * @ORM\Column(name="amount", type="decimal", precision=5, scale=2)
     */
    private $amount;

    /**
     * @var string
     *
     * @ORM\Column(name="tax_amount", type="decimal", precision=5, scale=2)
     */
    private $taxAmount;

    /**
     * @var string
     *
     * @ORM\Column(name="discount_amount", type="decimal", precision=5, scale=2)
     */
    private $discountAmount;

    ...
}

MySQL

_____________________________________________________________
|id                           | order id                    |
|order_id                     | fk order.id NOT NULL        |
|product_id                   | fk product.id               |
|description                  | varchar product description |
|count                        | int count                   |
|date                         | date                        |
|amount                       | amount                      |
|taxAmount                    | tax amount                  |
|discountAmount               | discount amount             |
|___________________________________________________________|

Я хотел бы создать одну форму, которая позволяет редактировать один порядок и строки.

OrderType.php   

class OrderType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('customer', 'entity', array(
                'class' => 'Customer',
                'multiple' => false
            ))
            ->add('orderStatus', 'entity', array(
                'class' => 'AppBundle\Entity\OrderStatus',
                'multiple' => false
            ))
            ->add('date')
            ->add('reference')
            ->add('comments')
            ->add('orderRows', 'collection', [
                'type' => new OrderRowType(),
                'allow_add' => true,
                'by_reference' => false,
            ])
        ;
    }

    ...
}

OrderRowType.php   

class OrderRowType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('order', 'entity', array(
                'class' => 'MyShop\Bundle\OrderBundle\Entity\Order',
                'multiple' => false
            ))
            ->add('product', 'product_selector') // service
            ->add('orderRowStatus', 'entity', array(
                'class' => 'AppBundle\Entity\OrderRowStatus',
                'multiple' => false
            ))
            ->add('description')
            ->add('count')
            ->add('startDate')
            ->add('endDate')
            ->add('amount')
            ->add('taxAmount')
            ->add('discountAmount')
        ;
    }

    ...
}

Обновление заказа выполняется путем отправки запроса на мой API:

  • URL-адрес запроса: https://api.example.net/admin/orders/update/37
  • Метод запроса: POST
  • Код состояния: 200

    Params: {
    
    "order[customer]": "3",
    "order[orderStatus]": "1",
    "order[date][month]:": "5",
    "order[date][day]": "18",
    "order[date][year]": "2015",
    "order[reference]": "Testing",
    "order[comments]": "I have nothing to say!",
    "order[orderRows][0][order]": "32",
    "order[orderRows][0][product]": "16721",
    "order[orderRows][0][orderRowStatus]:1": "1",
    "order[orderRows][0][description]": "8 GB memory",
    "order[orderRows][0][count]": "12",
    "order[orderRows][0][startDate][month]": "5",
    "order[orderRows][0][startDate][day]": "18",
    "order[orderRows][0][startDate][year]": "2015",
    "order[orderRows][0][endDate][month]": "5",
    "order[orderRows][0][endDate][day]": "18",
    "order[orderRows][0][endDate][year]": "2015",
    "order[orderRows][0][amount]": "122.03",
    "order[orderRows][0][taxAmount]": "25.63",
    "order[orderRows][0][discountAmount]": "0",
    "order[orderRows][1][order]": "32",
    "order[orderRows][1][product]": "10352",
    "order[orderRows][1][orderRowStatus]": "2",
    "order[orderRows][1][description]": "12 GB MEMORY",
    "order[orderRows][1][count]": "1",
    "order[orderRows][1][startDate][month]": "5",
    "order[orderRows][1][startDate][day]": "18",
    "order[orderRows][1][startDate][year]": "2015",
    "order[orderRows][1][endDate][month]": "5",
    "order[orderRows][1][endDate][day]": "18",
    "order[orderRows][1][endDate][year]": "2015",
    "order[orderRows][1][amount]": "30.8",
    "order[orderRows][1][taxAmount]": "6.47",
    "order[orderRows][1][discountAmount]": "0",
    "order[orderRows][2][order]": "32",
    "order[orderRows][2][product]": "2128",
    "order[orderRows][2][orderRowStatus]": "3",
    "order[orderRows][2][description]": "4GB MEMORY",
    "order[orderRows][2][count]": "5",
    "order[orderRows][2][startDate][month]": "5",
    "order[orderRows][2][startDate][day]": "18",
    "order[orderRows][2][startDate][year]": "2015",
    "order[orderRows][2][endDate][month]": "5",
    "order[orderRows][2][endDate][day]": "18",
    "order[orderRows][2][endDate][year]": "2015",
    "order[orderRows][2][amount]": "35.5",
    "order[orderRows][2][taxAmount]": "7.46",
    "order[orderRows][2][discountAmount]": "0"
    }
    

Приведенный выше запрос редактирует детали заказа и создает новый order_rows, поскольку не было заказано order_row_id. Nowere в Symfony2, я обнаружил, что мне нужно просто $builder- > добавить ('id') в свой OrderRowType, а также мои объекты не имеют сеттеров для идентификатора столбца.

После большого количества информации у меня очень короткий вопрос. Как мне обновить записи order_rows в этой форме?

4b9b3361

Ответ 1

Работа с коллекцией и Doctrine может когда-нибудь быть сложной, если вы не знаете внутренности. Сначала я расскажу вам о внутренних деталях, чтобы вы получили более четкое представление о том, что сделано под капотом.

Трудно оценить фактическую проблему из деталей, которые вы дали, но я даю вам несколько советов, которые могут помочь вам отладить проблему. Я даю обширный ответ, поэтому он может помочь другим.

TL; версия DR

Вот моя догадка: вы изменяете объект по ссылке, даже если вы установили by_reference в false. Вероятно, это связано с тем, что вы не определили методы addOrderRow и removeOrderRow (оба из них) или потому, что вы не используете объект коллекции доктрины

Некоторые внутренние элементы

Форма

Когда вы создаете объект Form в своем контроллере, вы привязываете его к объекту, который вы извлекли из базы данных (то есть с идентификатором) или только что создали: это означает, что для формы НЕ требуется идентификатор основных объектов, а также не требует идентификаторов объекта коллекции. Вы можете добавить его в формы для вашего удобства, но если вы убедитесь, что они неизменяемы (например, hidden введите с опцией disabled => true).

Когда создается форма Collection, Symfony автоматически создает одну подформу для каждого объекта, уже присутствующего в коллекции сущностей; поэтому в действии entity/<id>/edit вы всегда должны видеть редактируемую форму для уже существующего элемента коллекции.

Параметры allow_add и allow_delete определяют, можно ли динамически изменять размер сгенерированной подформы, удаляя какой-либо элемент коллекции или добавляя новые элементы (см. класс ResizeFormListener). Обратите внимание, что при использовании prototype с javascript следует использовать тег __prototype__: это фактический key, который используется для переназначения серверной части объекта, поэтому, если вы измените его, форма создаст новый элемент в коллекции.

Учение

В Доктрине вам нужно хорошо позаботиться о owning side и inverse side отображения. Сторона owning является сущностью, которая будет сохранять связь с базой данных, а обратная сторона - другой. При сохранении сторона owning является ТОЛЬКО, которая запускает отношение, которое нужно сохранить. Это модельная ответственность за синхронизацию обоих отношений во время модификации объекта.

При работе со отношениями "один ко многим" сторона owning является many (например, OrderRow в вашем случае), а one - стороной inverse.

Наконец, приложение должно явно помечать объекты, которые необходимо сохранить. Обе стороны отношения могут быть отмечены как persist cascading, так что все доступные объекты через отношения сохраняются. Во время этого процесса все новые объекты автоматически сохраняются и (в стандартной конфигурации) обновляются все "грязные" объекты.

Концепция грязной сущности хорошо объясняется в официальных документах. По умолчанию Doctrine автоматически обнаруживает обновленные объекты, сравнивая каждое свойство с исходным состоянием и генерируя оператор UPDATE во время флеша. Если это сделано явно для повышения производительности (т.е. @ChangeTrackingPolicy("DEFERRED_EXPLICIT")), все сущности должны сохраняться вручную, даже если отношение отмечено как каскадное.

Также обратите внимание, что когда объекты перезагружаются из БД, Doctrine использует экземпляр PersistenCollection для обработки коллекции, поэтому вам необходимо использовать интерфейс коллекции доктрины для обработки коллекции объектов.

Что нужно проверить

Подводя итог, здесь (надеюсь, полный) список вещей, чтобы проверить правильное обновление коллекции.

Обе стороны отношений Доктрина установлены правильно

  • как сторона-обладатель, так и обратная сторона должны быть отмечены как каскадные данные (если контроллер не должен вручную каскадно... не рекомендуется, если не слишком медленно);
  • свойство коллекции ДОЛЖНО быть реализацией Doctrine\Common\Collection, а не простого массива;
  • модели должны взаимно обновляться при каждом изменении, поэтому это означает, что
  • объект коллекции НЕ ДОЛЖЕН быть возвращен как есть, чтобы избежать модификации по ссылке.

В вашем случае:

<?php

class Order
{
    /**
    * @var integer
    *
    * @ORM\Column(name="id", type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
     * @var \Doctrine\Common\Collections\Collection
     * @ORM\OneToMany(targetEntity="OrderRow", mappedBy="Order", cascade={"persist"})
     */
    private $orderRows;

    public function __construct()
    {
        // this is required, as Doctrine will replace it by a PersistenCollection on load
        $this->orderRows = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Add order row
     *
     * @param  OrderRow $row
     */
    public function addOrderRow(OrderRow $row)
    {
        if (! $this->orderRows->contains($row))
            $this->orderRows[] = $row;

        $row->setOrder($this);
    }

    /**
     * Remove order row
     *
     * @param OrderRow $row
     */
    public function removeOrderRow(OrderRow $row)
    {
        $removed = $this->orderRows->removeElement($row);
        /*
        // you may decide to allow your domain to have spare rows, with order set to null
        if ($removed)
            $row->setOrder(null);
        */

        return $removed;
    }

    /**
     * Get order rows
     * @return OrderRow[]
     */
    public function getOrders()
    {
        // toArray prevent edit by reference, which breaks encapsulation
        return $this->orderRows->toArray();
    }
}

class OrderRows
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var Order
     * @ORM\ManyToOne(targetEntity="Order", inversedBy="orderRows", cascade={"persist"})
     * @ORM\JoinColumn(name="order_id, referencedColumnName="id", nullable=false)
     */
    private $order;


    /**
     * Set order
     *
     * @param  Order $order
     */
    public function setOrder(Order $order)
    {
        // avoid infinite loops addOrderRow -> setOrder -> addOrderRow
        if ($this->order === $order) {
            return;
        }

        if (null !== $this->order) {
            // see the comment above about spare order rows
            $this->order->removeOrderRow($this);
        }

        $this->order = $order;
    }

    /**
     * Get order
     *
     * @return Order
     */
    public function getOrder()
    {
        return $this->order;
    }
}

Сбор формы настроен правильно

  • Убедитесь, что форма заказа id не отображается формой (но включите в шаблон правильные параметры GET для действия маршрутизатора)
  • Убедитесь, что OrderRow order нет, так как это будет автоматически обновляться классом модели
  • убедитесь, что для параметра by_reference установлено значение false
  • убедитесь, что как addOrderRow, так и removeOrderRow определены в order классе
  • чтобы ускорить отладку, убедитесь, что Order::getOrderRows не возвращает коллекцию напрямую

Здесь фрагмент:

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('orderRows', 'collection', [
                'type'           => new OrderRowType(),
                'allow_add'      => true,  // without, new elements are ignored
                'allow_delete'   => true,  // without, deleted elements are not updated
                'by_reference'   => false, // hint Symfony to use addOrderRow and removeOrderRow
                                          // NOTE: both method MUST exist, or Symfony will ignore the option
            ])
        ;
    }
}

class OrderRowType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ->add('order') NOT required, the model will handle the setting
            ->add('product', 'product_selector') // service
        ;
    }
}

Контоллер должен правильно обновить объект

  • убедитесь, что форма создана правильно;
  • при использовании Form::handleRequest убедитесь, что HTTP-метод соответствует атрибуту метода формы
  • если форма действительна, позаботьтесь о удаленном элементе коллекции
  • если форма действительна, сохраняйте объект, затем очистите

В вашем случае у вас должно быть такое действие:

public function updateAction(Request $request, $id)
{
    $em = $this->getDoctrine()->getManager();

    $order = $em->getRepository('YourBundle:Order')->find($id);

    if (! $order) {
        throw $this->createNotFoundException('Unable to find Order entity.');
    }

    $previousRows = $order->getOrderRows();

// is a PUT request, so make sure that <input type="hidden" name="_method" value="PUT" /> is present in the template
    $editForm = $this->createForm(new OrderType(), $order, array(
        'method' => 'PUT',
        'action' => $this->generateUrl('order_update', array('id' => $id))
    ));
    $editForm->handleRequest($request);

    if ($editForm->isValid()) {
        // removed rows = previous rows - current rows
        $rowsRemoved = array_udiff($previousRows, $order->getOrderRows(), function ($a, $b) { return $a === $b ? 0 : -1; });

        // removed rows must be deleted manually
        foreach ($rowsRemoved as $row) {
            $em->remove($row);
        }

        // if not cascading, all rows must be persisted as well
        $em->flush();
    }

    return $this->render('YourBundle:Order:edit.html.twig', array(
        'entity'      => $order,
        'edit_form'   => $editForm->createView(),
    ));
}

Надеюсь, это поможет!

Ответ 2

Я не считаю, что это возможно по следующей причине:

OrderRows идентифицируются только по их идентификатору, поэтому для того, чтобы Doctrine узнала, какой объект был фактически обновлен, идентификатор должен быть известен. Но тогда вам нужно будет добавить идентификатор OrderRow в качестве поля, которое вы не хотите делать, потому что это позволит изменить "иностранные" OrderRows, которые не принадлежат к порядку. (без сложной проверки разрешений)

Решение состоит в том, чтобы полностью удалить старые OrderRows и вставить новые. Вставка уже работает: -).

Удаление объектов описано в cookbook в разделе Доктрина: обеспечение устойчивости базы данных

Есть только небольшой недостаток: OrderRows получает новый идентификатор при обновлении заказа.

Ответ 3

MappedBy должен быть o rder, а не O rder, поскольку он указывает на свойство, а не на имя класса.

    /**
 * @var array
 *
 * @ORM\OneToMany(targetEntity="OrderRow", mappedBy="order", cascade={"persist"})
 */
private $orderRows;