Ravioli87
Ravioli87

Reputation: 835

Symfony2 + Doctrine2: Entity Removal with Collections

Background Info

I have an entity called AnnualReport with multiple collections (let's say 2 for brevity's sake). Removal of one of these collections is handled automatically in the FormType:

        //AnnualReportStaffing entity collection
        ->add('staffingTenured', 'collection', array(
            'type' => new AnnualReportStaffingType(),
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' => false,
        ))

The other collection is a collection of files and deletion is NOT handled automatically:

        //AnnualReportDetail entity collection
        ->add('documents', 'collection', array(
            'type' => new AnnualReportDocumentType(),
            'allow_add' => true,
            'allow_delete' => false, // Do NOT automatically remove documents not in the collection (i.e. edit form where Documents are not passed again)
            'by_reference' => false,
        ))

This is the property/method declaration of each collection within my AnnualReport entity class:

/**
 * @ORM\ManyToMany(targetEntity="AnnualReportStaffing", cascade={"persist", "detach", "remove"}, orphanRemoval=true, fetch="LAZY")
 * @ORM\JoinTable(name="annualreports_staffingtenure",
 *      joinColumns={@ORM\JoinColumn(name="annualreport_id", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="staffing_id", referencedColumnName="id", onDelete="CASCADE")},
 *      )
 */
private $staffingTenured;

/**
 * @ORM\ManyToMany(targetEntity="Document", cascade={"persist", "detach", "remove"}, orphanRemoval=true, fetch="LAZY")
 * @ORM\JoinTable(name="annualreports_documents",
 *      joinColumns={@ORM\JoinColumn(name="annualreport_id", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="document_id", referencedColumnName="id", onDelete="CASCADE")},
 *      )
 */
private $documents;

public function __construct(AnnualReportUnit $unit, $year) {
    $this->staffingTenured = new ArrayCollection();
    $this->documents = new ArrayCollection();
}

/**
 * Add staffingTenured
 *
 * @param AppBundle\Entity\AnnualReportStaffing  $staffing
 * @return AnnualReport
 */
public function addStaffingTenured(AnnualReportStaffing $staffing)
{
    $this->staffingTenured->add($staffing);

    return $this;
}

/**
 * Remove staffingTenured
 *
 * @param AppBundle\Entity\AnnualReportStaffing  $staffing
 * @return AnnualReport
 */
public function removeStaffingTenured(AnnualReportStaffing $staffing)
{
    $this->staffingTenured->removeElement($staffing);

    return $this;
}

/**
 * Get staffingTenured
 *
 * @return ArrayCollection 
 */
public function getStaffingTenured()
{
    return $this->staffingTenured;
}

/**
 * Add document
 *
 * @param AppBundle\Entity\AnnualReportDocument  $document
 * @return AnnualReport
 */
public function addDocument(AnnualReportDocument $document)
{
    $this->documents->add($document);

    return $this;
}

/**
 * Remove document
 *
 * @param AppBundle\Entity\AnnualReportDocument  $document
 * @return AnnualReport
 */
public function removeDocument(AnnualReportDocument $document)
{
    $this->documents->removeElement($document);

    return $this;
}

/**
 * Get documents
 *
 * @return ArrayCollection
 */
public function getDocuments()
{
    return $this->documents;
}

Problem

When it comes time to delete an AnnualReport entity:

An exception occurred while executing 'DELETE FROM annual_report WHERE id = ?' with params [57]:

SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails

(libcommand.annualreports_staffingtenure, CONSTRAINT FK_DB56517AD4F67A27 FOREIGN KEY (annualreport_id) REFERENCES annual_report (id))

This is the deleteAction():

public function deleteAction(Request $request, $id)
{
    $requestData = $request->request->all();
    $unit = $requestData['form']['unit'];


    $form = $this->createDeleteForm($id);
    $form->handleRequest($request);

    if ($form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $entity = $em->getRepository('AppBundle:AnnualReport')->find($id);

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

        //Remove any documents (allow_delete set to FALSE in form so have to do manually)
        $documents = $entity->getDocuments();
        foreach($documents as $document){
            $entity->removeDocument($document);
            //$em->remove($document);
        }
        $em->persist($entity);
        $em->flush();

        $em->remove($entity);
        $em->flush(); //flush again to remove the annual report
    }

    return $this->redirect($this->generateUrl('annualreportunit_edit', array('id' => $unit)));
}

Upvotes: 0

Views: 222

Answers (2)

Zuhayer Tahir
Zuhayer Tahir

Reputation: 1916

How about you catch a ForeignKeyConstraintViolationException exception or better yet catch DBALException. Then show the user that the error. Or when you catch the exception you can remove child nodes and then remove the entity again.

use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
Or
use Doctrine\DBAL\DBALException;

try {
    $em->remove($entity);
    $em->flush();

    $this->addFlash('success', 'Removed');
} catch (DBALException $e) {
    $em->remove($childEntity);
    $em->flush();

    $em->remove($entity);
    $em->flush();
--OR--
} catch (ForeignKeyConstraintViolationException $e) {
    $em->remove($childEntity);
    $em->flush();

    $em->remove($entity);
    $em->flush();
}

Upvotes: 0

Jakub Matczak
Jakub Matczak

Reputation: 15696

You are mixing forms and ORM here. allow_delete is a form's parameter which means it's used to handle forms.

As Symfony's doc say about allow_delete:

If set to true, then if an existing item is not contained in the submitted data, it will be correctly absent from the final array of items. This means that you can implement a "delete" button via JavaScript which simply removes a form element from the DOM. When the user submits the form, its absence from the submitted data will mean that it's removed from the final array.

So it can be used to implement possibility of removing collection items from the root entity in the form. But it doesn't mean that Doctrine will magically handle it too.

If you want child entities to be saved/deleted when parent entity is, you should use cascade attribute in entity mapping.

Upvotes: 1

Related Questions