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

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(),
));
}

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

Since this post is 2 years old I don't know if help is still needed here, anyway..
You should place an addIngredient function within your Dish entity, this function checks if the Ingredient object exists within the current ingredient collection.
addIngredient(Ingredient $ingredient){
if (!$this->ingredients->contains($ingredient)) {
$this->ingredients[] = $ingredient;
}
}
Hopefully it could still help you out.

Related

EntityType multiselect for ManyToMany does not remove relations if deselect an option

Having a ManyToMany relation between Article and Tag i have a form with a multiselect of type EntityType using Symfony FormBuilder.
Everything seems fine, even preselection, except when deselecting an already related Tag from the tags-select, it does not remove the underlying relation. Therefor I can only add more relations.
I tried removing all relations before saving but because of LazyLoading I get other trouble there. I cannot find anything regarding this in the documentation but this might be very common basics, right?
Can anyone help me out here?
This is the code of my EntityType-Field
$builder->add('tags', FormEntityType::class, [
'label' => 'Tags',
'required' => false,
'multiple' => true,
'expanded' => false,
'class' => Tag::class,
'choice_label' => function (Tag $tag) {
return $tag->getName();
},
'query_builder' => function (TagRepository $er) {
return $er->createQueryBuilder('t')
->where('t.isActive = true')
->orderBy('t.sort', 'ASC')
->addOrderBy('t.name', 'ASC');
},
'choice_value' => function(?Tag $entity) {
return $tag ? $tag->getId() : '';
},
'choice_attr' => function ($choice) use ($options) {
// Pre-Selection
if ($options['data']->getTags() instanceof Collection
&& $choice instanceof Tag) {
if ($options['data']->getTags()->contains($choice)) {
return ['selected' => 'selected'];
}
}
return [];
}
]);
And this is how I save after form-submit
$articleForm = $this->createForm(ArticleEditFormType::class, $article)->handleRequest($this->getRequest());
if ($articleForm->isSubmitted() && $articleForm->isValid()) {
// saving the article
$article = $articleForm->getData();
$this->em->persist($article);
$this->em->flush();
}
The Relations look like this:
Article.php
/**
* #ORM\ManyToMany(targetEntity=Tag::class, mappedBy="articles", cascade={"remove"})
* #ORM\OrderBy({"sort" = "ASC"})
*/
private $tags;
public function removeTag(Tag $tag): self
{
if ($this->tags->removeElement($tag)) {
$tag->removeArticle($this);
}
return $this;
}
Tag.php
/**
* #ORM\ManyToMany(targetEntity=Article::class, inversedBy="tags", cascade={"remove"})
* #ORM\OrderBy({"isActive" = "DESC","name" = "ASC"})
*/
private $articles;
public function removeArticle(Article $article): self
{
$this->articles->removeElement($article);
return $this;
}
Setters, adders, etc. won't be called on the entity by default.
In the case of a multiselect where the underlying object is a collection, to ensure that the element is removed by calling removeArticle you have to set the option by_reference to false.
This is applicable to CollectionType as well.

Symfony Forms: CollectionType - Don't transform null to empty array

We are using Symfony Forms for our API to validate request data. At the moment we are facing a problem with the CollectionType which is converting the supplied value null to an empty array [].
As it is important for me to differentiate between the user suppling null or an empty array I would like to disable this behavior.
I already tried to set the 'empty_data' to null - unfortunately without success.
This is how the configuration of my field looks like:
$builder->add(
'subjects',
Type\CollectionType::class,
[
'entry_type' => Type\IntegerType::class,
'entry_options' => [
'label' => 'subjects',
'required' => true,
'empty_data' => null,
],
'required' => false,
'allow_add' => true,
'empty_data' => null,
]
);
The form get's handled like this:
$data = $apiRequest->getData();
$form = $this->formFactory->create($formType, $data, ['csrf_protection' => false, 'allow_extra_fields' => true]);
$form->submit($data);
$formData = $form->getData();
The current behavior is:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => [] }
My desired behavior would be:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => null }
After several tries I finally found a solution by creating a From Type Extension in combination with a Data Transformer
By creating this form type extension I'm able to extend the default configuration of the CollectionType FormType. This way I can set a custom build ModelTransformer to handle my desired behavior.
This is my Form Type Extension:
class KeepNullFormTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer(new KeepNullDataTransformer());
}
}
This one needs to be registered with the 'form.type_extension' tag in your service.yml:
PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension:
class: PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension
tags: ['form.type_extension']
Please note that you still use the CollectionType in your FormType and not the KeepNullFormTypeExtension as Symfony takes care about the extending...
In the KeepNullFormTypeExtension you can see that I set a custom model transformer with addModelTransformer which is called KeepNullDataTransformer
The KeepNullDataTransformer is responsible for keeping the input null as the output value - it looks like this:
class KeepNullDataTransformer implements DataTransformerInterface
{
protected $initialInputValue = 'unset';
/**
* {#inheritdoc}
*/
public function transform($data)
{
$this->initialInputValue = $data;
return $data;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->initialInputValue === null) ? null : $data;
}
}
And that's it - this way a supplied input of the type null will stay as null.
More details about this can be found in the linked Symfony documentation:
https://symfony.com/doc/current/form/create_form_type_extension.html
https://symfony.com/doc/2.3/cookbook/form/data_transformers.html

sonata Admin current entity

How can i access the id of a sonata admin entity. suppose i have an entity EndPoint which have a function getProject(), how can i get the id of that project. I tried the code bellow but this gave me the following error: Attempted to call an undefined method named "getProject" of class "ContainerAgbGotv\srcApp_KernelDevDebugContainer".
class EndPointAdmin extends AbstractAdmin{
protected function configureFormFields(FormMapper $form)
{ $container = $this->getConfigurationPool()->getContainer();
$em = $container->getProject();
$array = [];
foreach ($em as $ems) {
if (!empty($ems->getEnv())) {
$array[$ems->getEnv()] = $ems->getEnv();
}}
$result = array_unique($array);
$form
->add('name',ChoiceType::class, [
'choices'=> $result,
'placeholder' => 'Choose an option',
'required' => false
])
->add('ipAdress',TextType::class)
->add('port',IntegerType::class)
;
}
thanks for helping.
In an admin you will have access to
$this->getSubject()
which will return you the current entity for that admin. It's worth noting configureFormFields is used for both creating and editing an entity so getSubject() may return null.

How to use Lock Component in Symfony3.4 with username

I am trying to use the lock component in symfony 3.4, like it is described on
https://symfony.com/doc/3.4/components/lock.html
I want to prevent multiple data changes from different users.
For example user1 is calling the same company form with data, then user2
How can I tell user2, that editing data is blocked by user1 (incl username) ?
UPDATE:
It is used in backend, where a lot of employees editing data of customers, order etc.
this form is just for editing. that means, if they want to update some data, they click "edit". They should be informed when another employee changes this record before the data is loaded into the form. It sometimes takes some time for the employee to change everything. If the employee receives a message when saving it, they have to go back,reload the data and start all over again.
an example out of my controller:
public function CompanyEdit(Request $request)
{
$error = null;
$company_id = $request->get('id');
// if (!preg_match('/^\d+$/', $company_id)){
// return $this->showError();
// }
$store = new SemaphoreStore();
$factory = new Factory($store);
$lock = $factory->createLock('company-edit-'.$company_id, 30);
if(!$lock->acquire()) {
//send output with username
// this data is locked by user xy
return 0;
}
$company = $this->getDoctrine()->getRepository(Company::class)->find($company_id);
$payment = $this->getDoctrine()->getRepository(Companypay::class)->findOneBy(array('company_id' => $company_id));
$form = $this->createFormBuilder()
->add('company', CompanyFormType::class, array(
'data_class' => 'AppBundle\Entity\Company',
'data' => $company
))
->add('payment', CompanyPayFormType::class, array(
'data_class' => 'AppBundle\Entity\CompanyPay',
'data' => $payment
))
->getForm();
$form->handleRequest($request);
$company = $form->get('company')->getData();
$payment = $form->get('payment')->getData();
if ($form->isSubmitted() && $form->isValid()) {
$event = new FormEvent($form, $request);
if ($payment->getCompanyId() == null) {
$payment->setCompanyId($company->getId());
}
try {
$this->getDoctrine()->getManager()->persist($company);
$this->getDoctrine()->getManager()->persist($payment);
$this->getDoctrine()->getManager()->flush();
$this->container->get('app.logging')->write('Kundendaten geändert', $company->getId());
} catch (PDOException $e) {
$error = $e->getMessage();
}
if (null === $response = $event->getResponse()) {
return $this->render('customer/edit.html.twig', [
'form' => $form->createView(),
'company' => $company,
'error' => $error,
'success' => true
]);
}
$lock->release();
return $response;
}
You can't (Locks can't have any metadata), but you probably don't want this in the first place.
In this case, you create a Lock when a user opens the edit page and release it when a user submits the form. But what if the users opens the page and doesn't submit the form? And why can't a user even view the form?
This looks like a XY-problem. I think you're trying to prevent users to overwrite data without knowing. Instead, you can add a timestamp or hash to the form that changes after changing the entity. For example:
<form type="hidden" name="updatedAt" value="{{ company.updatedAt()|date('U') }}" />
And in your form:
<?php
if ($company->getUpdatedAt()->format('U') !== $form->get('updatedAt')->getData()) {
throw new \LogicException('The entity has been changed after you opened the page');
}
Disclaimer: code is not tested and just as an example how this solution can look like.

How to pass correctly object using redirectToRoute?

This is my simple code
class LuckyController extends Controller
{
public function taskFormAction(Request $request)
{
$task = new Task();
//$task->setTask('Test task');
//$task->setDueDate(new \DateTime('tomorrow noon'));
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, array('label' => 'Save'))
->getForm();
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid())
{
$task = $form->getData();
return $this->redirectToRoute('task_ok', array('task' => '123'));
}
return $this->render('pre.html.twig', array('pre' => print_r($task, true), 'form' => $form->createView()));
}
public function taskOKAction(Task $task)
{
return $this->render('ok.html.twig', array('msg' => 'ok', 'task' => print_r($task, true)));
}
}
and this line
return $this->redirectToRoute('task_ok', array('task' => '123'));
makes redirection to taskOKAction, but it lets me just send parameters by URL (?task=123).
I need to send object $task to taskOKAction to print on screen what user typed in form.
How can I do that? I've already red on stackoverflow before asking that the good solution is to store data from form (e.g. in database or file) and just pass in parameter in URL the ID of object. I think it's quite good solution but it adds me responsibility to check if user didn't change ID in URL to show other object.
What is the best way to do that?
Best regards,
L.
Use the parameter converter and annotate the route properly.
Something like (if you use annotation for routes)
/**
* #Route("/task/ok/{id}")
* #ParamConverter("task", class="VendorBundle:Task")
*/
public function taskOKAction(Task $task)
You can also omit the #ParamConverter part if parameter is only one and if is type hinted (as in your case)
From your form action you can do it without actual 302 redirect:
public function taskFormAction(Request $request)
{
$task = new Task();
//$task->setTask('Test task');
//$task->setDueDate(new \DateTime('tomorrow noon'));
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, array('label' => 'Save'))
->getForm();
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid())
{
$task = $form->getData();
return $this-taskOKAction($task)
}
}
This is perfectly legal, and your frontend colleague will thank you for lack of the redirects.

Resources