Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent duplicates in the database in a many-to-many relationship

I'm working on a back office of a restaurant's website. When I add a dish, I can add ingredients in two ways.

In my form template, I manually added a text input field. I applied on this field the autocomplete method of jQuery UI that allows:

  • Select existing ingredients (previously added)
  • Add new ingredients

However, when I submit the form, each ingredients are inserted in the database (normal behaviour you will tell me ). For the ingredients that do not exist it is good, but I don't want to insert again the ingredients already inserted.

Then I thought about Doctrine events, like prePersist(). But I don't see how to proceed. I would like to know if you have any idea of ​​the way to do it.

Here is the buildForm method of my DishType:

<?php 

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
    ->add('category', 'entity', array('class' => 'PrototypeAdminBundle:DishCategory',
                                      'property' => 'name',
                                      'multiple' => false ))
    ->add('title', 'text')
    ->add('description', 'textarea')
    ->add('price', 'text')
    ->add('ingredients', 'collection', array('type'        => new IngredientType(),
                                             'allow_add'    => true,
                                             'allow_delete' => true,
                                            ))

    ->add('image', new ImageType(), array( 'label' => false ) );
}

and the method in my controller where I handle the form :

<?php
public function addDishAction()
{

    $dish = new Dish();
    $form = $this->createForm(new DishType, $dish);

    $request = $this->get('request');

    if ($request->getMethod() == 'POST') {
        $form->bind($request);

        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($dish);
            $em->flush();

            return $this->redirect($this->generateUrl('prototype_admin_get_dish', array('slug' => $dish->getSlug())));
        }
    }

    return $this->render('PrototypeAdminBundle:Admin:addDish.html.twig', array(
        'form' => $form->createView(),
        ));
}
like image 836
Adrien G Avatar asked Jan 22 '14 10:01

Adrien G


1 Answers

I was having the same problem. My entities were projects (dishes in your case) and tags (ingredients).

I solved it by adding an event listener, as explained here.

services:
    my.doctrine.listener:
        class: Acme\AdminBundle\EventListener\UniqueIngredient
        tags:
            - { name: doctrine.event_listener, event: preUpdate }
            - { name: doctrine.event_listener, event: prePersist }

The listener triggers both prePersist (for newly added dishes) and preUpdate for updates on existing dishes.

The code checks if the ingredient already exists. If the ingredient exists it is used and the new entry is discarded.

The code follows:

<?php

namespace Acme\AdminBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;

use Acme\AdminBundle\Entity\Dish;
use Acme\AdminBundle\Entity\Ingredient;

class UniqueIngredient
{

    /**
     * This will be called on newly created entities
     */
    public function prePersist(LifecycleEventArgs $args)
    {

        $entity = $args->getEntity();

        // we're interested in Dishes only
        if ($entity instanceof Dish) {

            $entityManager = $args->getEntityManager();
            $ingredients = $entity->getIngredients();

            foreach($ingredients as $key => $ingredient){

                // let's check for existance of this ingredient
                $results = $entityManager->getRepository('Acme\AdminBundle\Entity\Ingredient')->findBy(array('name' => $ingredient->getName()), array('id' => 'ASC') );

                // if ingredient exists use the existing ingredient
                if (count($results) > 0){

                    $ingredients[$key] = $results[0];

                }

            }

        }

    }

    /**
     * Called on updates of existent entities
     *  
     * New ingredients were already created and persisted (although not flushed)
     * so we decide now wether to add them to Dishes or delete the duplicated ones
     */
    public function preUpdate(LifecycleEventArgs $args)
    {

        $entity = $args->getEntity();

        // we're interested in Dishes only
        if ($entity instanceof Dish) {

            $entityManager = $args->getEntityManager();
            $ingredients = $entity->getIngredients();

            foreach($ingredients as $ingredient){

                // let's check for existance of this ingredient
                // find by name and sort by id keep the older ingredient first
                $results = $entityManager->getRepository('Acme\AdminBundle\Entity\Ingredient')->findBy(array('name' => $ingredient->getName()), array('id' => 'ASC') );

                // if ingredient exists at least two rows will be returned
                // keep the first and discard the second
                if (count($results) > 1){

                    $knownIngredient = $results[0];
                    $entity->addIngredient($knownIngredient);

                    // remove the duplicated ingredient
                    $duplicatedIngredient = $results[1];
                    $entityManager->remove($duplicatedIngredient);

                }else{

                    // ingredient doesn't exist yet, add relation
                    $entity->addIngredient($ingredient);

                }

            }

        }

    }

}

NOTE: This seems to be working but I am not a Symfony / Doctrine expert so test your code carefully

Hope this helps!

pcruz

like image 108
umadesign Avatar answered Nov 15 '22 08:11

umadesign