php - generate - symfony database config
Symfony2 actualizando elementos en “subformulario” (3)
Versión corta de mi pregunta:
¿Cómo puedo editar entidades de subformularios en Symfony2?
= - = - = - = - = - = - = Versión larga y detallada = - = - = - = - = - = - = - =
Tengo una orden de entidad
<?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 | |___________________________________________________________|
Y una entidad OrderRow (una orden puede tener una o más filas)
<?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 | |___________________________________________________________|
Me gustaría crear un formulario que permita editar un pedido y sus filas.
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'')
;
}
...
}
La actualización de un pedido se realiza mediante el envío de una solicitud a mi API:
- Solicite la URL: https://api.example.net/admin/orders/update/37
- Método de solicitud: POST
Código de estado: 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" }
La solicitud anterior edita los detalles del pedido y crea nuevos order_rows, porque no se ha proporcionado order_row_id. En Symfony2, encontré que solo debería $ builder-> agregar (''id'') a mi OrderRowType, y mis entidades no tienen configuradores para el ID de columna.
Después de mucha información, tengo una pregunta muy corta. ¿Cómo debo actualizar los registros de order_rows dentro de este formulario?
El mappedBy debe ser mayor y no menor, ya que apunta a una propiedad y no a un nombre de clase.
/**
* @var array
*
* @ORM/OneToMany(targetEntity="OrderRow", mappedBy="order", cascade={"persist"})
*/
private $orderRows;
No creo que esto sea posible por la siguiente razón:
Los OrderRows solo se identifican por su identificación, por lo que para que Doctrine sepa qué entidad se actualizó realmente, la identificación debe ser conocida. Pero luego deberá agregar la ID de OrderRow como un campo, lo que no desea hacer porque esto permitirá el cambio de OrderRows ''extranjeras'' que no pertenecen a la orden. (sin complicada verificación de permisos)
La solución sería eliminar completamente los viejos OrderRows e insertar nuevos. La inserción ya funciona :-).
La eliminación de las entidades se describe en el libro de cocina bajo Doctrina: Asegurar la persistencia de la base de datos
Solo hay una pequeña desventaja: OrderRows obtiene nuevos identificadores cuando se actualiza un pedido.
Tratar con la recopilación y la Doctrina puede ser complejo en algún momento si no conoce los aspectos internos. Primero le daré información sobre los aspectos internos para que tenga una idea más clara de lo que se hace debajo del capó.
Es difícil estimar el problema real a partir de los detalles que proporcionó, pero le doy algunos consejos que pueden ayudarlo a solucionar el problema. Doy una respuesta extensa, por lo que posiblemente puede ayudar a otros.
TL; versión DR
Aquí están mis conjeturas: está modificando la entidad por referencia, incluso si establece by_reference
en falso. Probablemente esto se deba a que no ha definido los métodos addOrderRow
y removeOrderRow
(ambos) o porque no está utilizando el objeto de colección de doctrine
Algunos internos
Formar
Cuando crea un objeto de formulario en su controlador, lo vincula con una entidad que recuperó de la base de datos (es decir, con un ID), o que acaba de crear: esto significa que el formulario NO requiere el ID de las entidades principales, ni requiere los identificadores del objeto de colección. Puede agregarlo a los formularios para su conveniencia, pero si se asegura de que sean inmutables (p. Ej., El tipo hidden
con la opción disabled => true
).
Cuando se crea un formulario de colección, Symfony crea automáticamente un subformulario para cada entidad ya presente en la colección de entidades; esta es la razón por la que en una acción de entity/<id>/edit
(debería) ver siempre el formulario editable para el elemento de la colección ya presente.
Las opciones allow_add
y allow_delete
, controlan si el subformulario generado se puede redimensionar dinámicamente, eliminando algún elemento de la colección o agregando nuevos elementos (consulte la clase ResizeFormListener
). Tenga en cuenta que cuando use el prototype
con javascript, el marcador de posición __prototype__
debe usarse con cuidado: esta es la key
real que se usa para reasignar el lado del servidor de objetos, por lo que si lo cambia, el Formulario creará un nuevo elemento en la colección.
Doctrina
En Doctrine necesitas cuidar bien el owning side
y el inverse side
del mapeo. El lado owning
es la entidad que persistirá la asociación a la base de datos, y el lado inverso es la otra entidad. Al persistir, el lado owning
es el ÚNICO que desencadena la relación que se guardará. Es una responsabilidad modelo mantener ambas relaciones sincronizadas, durante la modificación del objeto.
Cuando se trata de relaciones de uno a muchos, el lado owning
es el many
(por ejemplo, OrderRow
s en su caso), y el one
es el lado inverse
.
Finalmente, la aplicación necesita marcar explícitamente las entidades para que persistan. Ambos lados de la relación pueden marcarse como persist cascading
, de modo que también se conservan todas las entidades alcanzables a través de las relaciones. Durante este proceso, todas las entidades nuevas se conservan automáticamente y, en una configuración estándar, todas las entidades "sucias" se actualizan.
El concepto de entidad sucia se explica bien en los documentos oficiales . De forma predeterminada, Doctrine detecta automáticamente las entidades actualizadas, comparando cada propiedad con el estado original, y genera una instrucción UPDATE
durante el vaciado. Si esto se hace explícito para mejorar el rendimiento (es decir, @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
), todas las entidades deben persistir manualmente, incluso si la relación está marcada como en cascada.
También tenga en cuenta que cuando las entidades se vuelven a cargar desde la base de datos, Doctrine usa una instancia de PersistenCollection
para manejar la recopilación, por lo que debe usar la interfaz de recopilación de doctrina para manejar la recopilación de entidades.
Qué revisar
Para resumir, aquí hay una (esperemos que completa) lista de cosas para verificar la actualización de la colección adecuada.
Ambos lados de las relaciones de Doctrina se establecen correctamente
- tanto el lado propietario como el lado inverso deben marcarse como persistente en cascada (de lo contrario, el controlador debe conectarse en cascada manualmente ... no se recomienda, a menos que sea demasiado lento);
- la propiedad de colección DEBE ser una implementación de
Doctrine/Common/Collection
, no una simple matriz; - los modelos deben actualizarse mutuamente en cada cambio, por lo que esto implica que
- El objeto de colección NO DEBE devolverse como está para evitar modificaciones por referencia.
En tu caso:
<?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;
}
}
La colección de formularios está configurada correctamente.
- Asegúrese de que la
id
pedido no esté expuesta en el formulario (pero incluya en la plantilla los parámetrosGET
correctos para la acción del enrutador) - Asegúrese de que la
order
OrderRow no esté presente, ya que la clase modelo la actualizará automáticamente - asegúrese de que
by_reference
se establece enfalse
- asegúrese de que tanto
addOrderRow
comoremoveOrderRow
estén definidos en la claseOrder
- para acelerar la depuración, asegúrese de que
Order::getOrderRows
no devuelva la colección directamente
Aquí el fragmento:
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
;
}
}
El controlador debe actualizar adecuadamente la entidad.
- asegúrese de que el formulario se crea correctamente;
- Si usa
Form::handleRequest
asegúrese de que el Método HTTP coincida con el atributo del método Form. - Si el formulario es válido, cuidar del elemento eliminado de la colección.
- Si el formulario es válido, persistir la entidad luego vaciar
En tu caso deberías tener una acción como esta:
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(),
));
}
¡Espero que esto ayude!