migrations generate create php symfony doctrine-orm symfony-forms

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

  1. 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);
  2. la propiedad de colección DEBE ser una implementación de Doctrine/Common/Collection , no una simple matriz;
  3. los modelos deben actualizarse mutuamente en cada cambio, por lo que esto implica que
  4. 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.

  1. Asegúrese de que la id pedido no esté expuesta en el formulario (pero incluya en la plantilla los parámetros GET correctos para la acción del enrutador)
  2. Asegúrese de que la order OrderRow no esté presente, ya que la clase modelo la actualizará automáticamente
  3. asegúrese de que by_reference se establece en false
  4. asegúrese de que tanto addOrderRow como removeOrderRow estén definidos en la clase Order
  5. 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.

  1. asegúrese de que el formulario se crea correctamente;
  2. Si usa Form::handleRequest asegúrese de que el Método HTTP coincida con el atributo del método Form.
  3. Si el formulario es válido, cuidar del elemento eliminado de la colección.
  4. 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!