Ответ 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(),
));
}
Надеюсь, это поможет!