Symfony3 - Saving a collection - symfony

I have a little problem. I have an object "persona" who has several collections.
I have a form who render all the collection and allow me to add and remove collections.
After that, I dump all the information and the object "persona" has all the collection I sent it when I submitted the form.
When I persist and flush the data, doctrine saves persona but not the collection
This are my configurations:
Entity persona
/**
* #ORM\OneToMany(targetEntity="PersonaDomicilio",mappedBy="idPersona",cascade={"persist"},orphanRemoval=true)
*/
private $domicilios;
public function __construct() {
$this->domicilios = new ArrayCollection();
}
public function getDomicilios() {
return $this->domicilios;
}
public function addDomicilio(PersonaDomicilio $persona_domicilio) {
$persona_domicilio->setIdPersona($this);
$this->domicilios[] = $persona_domicilio;
}
public function removeDomicilio(PersonaDomicilio $persona_domicilio) {
$this->domicilios->removeElement($persona_domicilio);
}
Entity PersonaDomicilio
/**
* #var \AppBundle\Entity\Persona
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Persona",inversedBy="domicilios")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_persona", referencedColumnName="id_persona")
* })
*/
private $idPersona;
The PersonaType
->add('domicilios', CollectionType::class, array(
'entry_type' => PersonaDomicilioType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'label' => false
))
The controller action
$em = $this->getDoctrine()->getManager();
$persona = new Persona();
$formulario = $this->createForm(
PersonaType::class,
$persona,
array('action' => $this->generateUrl('persona_create'),
'method' => 'POST')
);
$formulario->handleRequest($request);
$persona->setFisicaJuridica('F');
$em->persist($persona);
$em->flush();
I don´t wanna persist all the collection manually with a foreach, because the cascade persist would help to do that.
I have to say that I did several tests and I can´t understand why is not working.
Pd: "id_persona" is correctly setted to the collections too.

This is "normal". If you refer to this old symfony documentation (which is still valid), you also have to persist your PersonaDomicilio (see the paragraph Doctrine: Ensuring the database persistence).
Here is their example :
// src/Acme/TaskBundle/Controller/TaskController.php
use Doctrine\Common\Collections\ArrayCollection;
// ...
public function editAction($id, Request $request)
{
$em = $this->getDoctrine()->getManager();
$task = $em->getRepository('AcmeTaskBundle:Task')->find($id);
if (!$task) {
throw $this->createNotFoundException('No task found for id '.$id);
}
$originalTags = new ArrayCollection();
// Create an ArrayCollection of the current Tag objects in the database
foreach ($task->getTags() as $tag) {
$originalTags->add($tag);
}
$editForm = $this->createForm(new TaskType(), $task);
$editForm->handleRequest($request);
if ($editForm->isValid()) {
// remove the relationship between the tag and the Task
foreach ($originalTags as $tag) {
if (false === $task->getTags()->contains($tag)) {
// remove the Task from the Tag
$tag->getTasks()->removeElement($task);
// if it was a many-to-one relationship, remove the relationship like this
// $tag->setTask(null);
$em->persist($tag);
// if you wanted to delete the Tag entirely, you can also do that
// $em->remove($tag);
}
}
$em->persist($task);
$em->flush();
// redirect back to some edit page
return $this->redirectToRoute('task_edit', array('id' => $id));
}
// render some form template
}
And here an example for your configuration (not tested)
<?php
// Create an ArrayCollection of the current domicilios objects in the database
$originalDomicilios = new ArrayCollection();
foreach ($persona->getDomicilios() as $domicilio) {
$originalDomicilios->add($domicilio);
}
// Check request
$form->handleRequest($request);
// Was the form submitted?
if ($form->isSubmitted() && $form->isValid()) {
try {
// Handle domicilios
foreach ($originalDomicilios as $domicilio) {
if (false === $persona->getDomicilios()->contains($domicilio)) {
// remove the persona from the domicilio (or remove it)
$domicilio->removePersona($persona);
// Persist domicilio
$em->persist($domicilio);
}
}
// Save new domicilios
foreach($persona->getDomicilios() as $domicilio){
if (false === $originalDomicilios->contains($domicilio)){
// Add persona
$domicilio->addPersona($persona);
// Persist domicilio
$em->persist($domicilio);
}
}
// Persist persona
$em->persist($persona);
// Save
$em->flush();
...
} catch (\Exception $e) {
}
}

Related

Merge form CollectionType with existing collection

I would need some help about management of CollectionType. In order to make my question as clear as possible, I will change my situation to fit the official Symfony documentation, with Tasks and Tags.
What I would like :
A existing task have alrady some tags assigned to it
I want to submit a list of tags with an additional field (value)
If the submitted tags are already assigned to the task->tags collection, I want to update them
It they are not, I want to add them to the collection with the submitted values
Existing tags, no part of the form, must be kept
Here is the problem :
All task tags are always overwritten by submitted data, including bedore the handleRequest method is called in the controller.
Therefore, I can't even compare the existing data using the repository, since this one already contains the collection sent by the form, even at the top of the update function in the controller.
Entity wize, this is a ManyToMany relation with an additional field (called value), so in reality, 2 OneToMany relations. Here are the code :
Entity "Task"
class Task
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'task', targetEntity: TaskTags::class, orphanRemoval: false, cascade: ['persist'])]
private Collection $TaskTags;
/**
* #return Collection<int, TaskTags>
*/
public function getTaskTags(): Collection
{
return $this->TaskTags;
}
public function addTaskTag(TaskTags $TaskTag): self
{
// I have voluntarily remove the presence condition during my tests
$this->TaskTags->add($TaskTag);
$TaskTag->setTask($this);
return $this;
}
public function removeTaskTag(TaskTags $TaskTag): self
{
if ($this->TaskTags->removeElement($TaskTag)) {
// set the owning side to null (unless already changed)
if ($TaskTag->getTask() === $this) {
$TaskTag->setTask(null);
}
}
return $this;
}
}
Entity "Tag"
class Tag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'tag', targetEntity: TaskTags::class, orphanRemoval: false)]
private Collection $TaskTags;
/**
* #return Collection<int, TaskTags>
*/
public function getTaskTags(): Collection
{
return $this->TaskTags;
}
public function addTaskTag(TaskTags $TaskTag): self
{
$this->TaskTags->add($TaskTag);
$TaskTag->setTag($this);
return $this;
}
public function removeTaskTag(TaskTags $TaskTag): self
{
if ($this->TaskTags->removeElement($TaskTag)) {
// set the owning side to null (unless already changed)
if ($TaskTag->getTag() === $this) {
$TaskTag->setTag(null);
}
}
return $this;
}
}
Entity "TaskTags"
class TaskTags
{
#[ORM\Id]
#[ORM\ManyToOne(inversedBy: 'TaskTags')]
#[ORM\JoinColumn(nullable: false)]
private Task $task;
#[ORM\Id]
#[ORM\ManyToOne(inversedBy: 'TaskTags')]
#[ORM\JoinColumn(nullable: false)]
private Tag $tag;
// The addional field
#[ORM\Column(nullable: true)]
private ?int $value = null;
public function getTask(): ?Task
{
return $this->task;
}
public function setTask(?Task $task): self
{
if(null !== $task) {
$this->task = $task;
}
return $this;
}
public function getTag(): ?Tag
{
return $this->tag;
}
public function setTag(?Tag $tag): self
{
if(null !== $tag) {
$this->tag = $tag;
}
return $this;
}
public function getValue(): ?string
{
return $this->value;
}
public function setValue(?string $value): self
{
$this->value = $value;
return $this;
}
}
FormType "TaskFormType"
class TaskFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
...
->add('TaskTags', CollectionType::class, [
'by_reference' => false,
'entry_type' => TaskTagsFormType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
'csrf_protection' => false
]);
}
}
FormType "TaskTagsFormType"
class TaskTagsFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('task')
->add('tag')
->add('value')
;
}
Controller
#[Route('/tasks/edit/{id}/tags', name: 'app_edit_task')]
public function editasktags(Request $request, EntityManagerInterface $em, TaskTagsRepository $TaskTagsRepo): Response
{
...
// Create an ArrayCollection of the current tags assigned to the task
$task = $this->getTask();
// when displaying the form (method GET), this collection shows correctly the tags already assigned to the task
// when the form is submitted, it immediately becomes the collection sent by the form
$ExistingTaskTags = $TaskTagsRepo->findByTask($task);
$form = $this->createForm(TaskFormType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// here begins all I have tried ... I was trying to compare the value in the DB and in the form, but because of the repo being overwritten I can't
$task = $form->getData();
$SubmittedTaskTags = $userForm->getTaskTags();
$CalculatedTaskTags = new ArrayCollection();
foreach ($ExistingTaskTags as $ExistingTaskTag) {
foreach ($SubmittedTaskTags as $SubmittedTaskTag) {
if ($ExistingTaskTag->getTag()->getId() !== $SubmittedTaskTag->getTag()->getId()) {
// The existing tag is not the same as submitted, keeping it as it in a new collection
$CalculatedTaskTags->add($ExistingTaskTag);
} else {
// The submitted tag is equal to the one in DB, so adding the submitted one
$SubmittedTaskTag->setTask($task);
$CalculatedTaskTags->add($SubmittedTaskTag);
}
}
}
$em->persist($task);
$em->flush();
}
return $this->render('task/edittasktags.twig.html', [
'form' => $form,
'task' => $this->getTask()
]);
}
My main issue is that I am not able to get the existing data one the form has been submitted, in order to perform a "merge"
I have tried so many things.
One I did not, and I'd like to avoid : sending the existing collection as hidden fields.
I don't like this at all since if the data have been modified in the meantime, we are sending outdated data, which could be a mess in multi tab usage.
Thank you in advance for your help, I understand this topic is not that easy.
NB : the code I sent it not my real code / entity. I've re written according to the Symfony doc case, so there could be some typo here and there, apologize.
Solution found.
I added 'mapped' => false in the FormType.
And I was able to retrieve the form data using
$SubmittedTags = $form->get('TaskTags')->getData();
The repository was not overwritten by the submitted collection.

How to modify user Information after requiring his password with Symfony 4

I would like to create a page that allows the user to modify his personal infos.
I want him to enter his current password to modify any information.
I created a form based on the connected user when the form is submitted and valid, I want to check if the password is valid using the function isPasswordValid() of my passwordEncoder..
My problem is that when this function is called with $user as parameter, it always returns false. I found where this problem comes from, it's because the $user used as parameter as been modified when the form has been submitted. I've tried declaring another variable to stock my Initial User ($dbUser for example) and using another to instance the form but when I dump the
$dbUser it has been modified and I don't know why...
The variable shouldn't be changed after the submit because it's never used... I can't find what I doing wrong...
/**
* #Route("/mes-infos", name="account_infos")
*/
public function showMyInfos(Request $request, UserRepository $userRepo)
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
// $dbUser = $userRepo->findOneBy(['id' => 13]);
$form = $this->createForm(UserModificationType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$enteredPassword = $request->request->get('user_modification')['plainPassword'];
$passwordEncoder = $this->passwordEncoder;
$manager = $this->getDoctrine()->getManager();
if ($passwordEncoder->isPasswordValid($user, $enteredPassword)) {
dd('it works!!!!!');
// $manager->persist($user);
// $manager->flush();
} else {
dd('It\'s not!!!!');
}
}
return $this->render('account/myaccount-infos.html.twig', [
'form' => $form->createView(),
]);
}
The better solution is to use a constraint.
Symfony already implements a UserPasswordConstraint.
You can add it in your entity directly. Be careful to declare a group for this constraint. If you don't do it, your use case "user update" will works fine, but the use case "user creation" will now failed.
namespace App\Entity;
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
class User
{
//...
/**
* #SecurityAssert\UserPassword(
* message = "Wrong value for your current password",
* groups = {"update"}
* )
*/
protected $password;
//...
}
In your form, specified the validation groups by updating (or adding) the configureOptions method:
//App\Form\UserModificationType
//...
class UserModificationType {
//...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ...
'validation_groups' => ['update'],
]);
}
}
Then your $form->isValid() will automatically test the password. So, you can removed all lines in the "if condition".
/**
* #Route("/mes-infos", name="account_infos")
*/
public function showMyInfos(Request $request, UserRepository $userRepo)
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
$form = $this->createForm(UserModificationType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd('Password is good! it works!!!!!');
}
return $this->render('account/myaccount-infos.html.twig', [
'form' => $form->createView()
]);
}
But I think it is a better practice to use a Model like in the documentation when you update your user entity.

Bind an object to a Symfony form

I have a entity with some data prefilled, and I want to display them on the form : here state would be ACTIVE and customerId = 1;
// Form file (ben.file.create)
protected function buildForm()
{
$this->formBuilder->add('name', 'text', ['label'=>'Nom du patient'])
->add('customerId', 'integer')
->add('state', 'choice', ['choices'=>['ACTIVE'=>'ACTIVE',
'DONE'=>'DONE', 'CANCELLED'=>'CANCELLED']]);
}
// in the controller
public function index()
{
$form = $this->createForm("ben-file-create");
$file = new BenFile();
$file->setCustomerId(1);
$file->setState('ACTIVE');
$form->getForm()->submit($file); // <--- Here the glue problem
return $this->render('create-file', array());
}
It looks like submit is not the right bind function. I would expect that the form is pre-filled accordingly, and after the POST request, I have an updated BenFile entity.
You can do easily in createForm method :
// in the controller
public function index(Request $request)
{
$file = new BenFile();
$file->setCustomerId(1);
$file->setState('ACTIVE');
$form = $this->createForm("ben-file-create", $file);
// handle submit
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// flush entity
$fileNew = $form->getData();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($fileNew );
$entityManager->flush();
}
return $this->render('create-file', array());
}

How to load 'isSubmitted()-method' (defined in service controller) into the main controller?

I've put a registration form in a method so, that I can use it in different places.
My service registration controller looks like this:
public function loadRegisterForm()
{
$user = new User();
$form = $this->createForm(RegistrationType::class, $user);
$form->handleRequest($this->request);
$errors = "";
if ($form->isSubmitted())
{
if ($form->isValid())
{
$password = $this->get('security.password_encoder')
->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$user->setIsActive(1);
$user->setLastname('none');
$user->setCountry('none');
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
}
else
{
$errors = $this->get('validator')->validate($form);
}
}
$parametersArray['form'] = $form;
$parametersArray['errors'] = $errors;
return $parametersArray;
}
services.yml looks like this:
register_form_service:
class: ImmoBundle\Controller\Security\RegistrationController
calls:
- [setContainer, ["#service_container"]]
And the main controller where I load the service controller:
private function indexAction()
{
/**
* Load register form
*/
$registerForm = $this->get('register_form_service');
$registerParameters = $registerForm->loadRegisterForm();
$registerParameters['form']->handleRequest($request);
return $this->render(
'ImmoBundle::Pages/mainPage.html.twig',
array(
'register_form' => $registerParameters['form']->createView(),
'errors' => $registerParameters['errors'],
)
);
}
The form itself is well rendered, so there is no problem. However nothing happens if I try to submit the form. I know that I should add the following line to the main controller
if ($registerParameters['form']->isSubmitted())
{
// add to db
}
But is there any way to do it only in a service controller?
You do not need a service definition to inject the container into your controller. If the controller extends Symfony\Bundle\FrameworkBundle\Controller\Controller all services are accesible via ->get(). Next to that, $form->isValid() already checks whether the form is submitted.
Why is your action private? It should be public, and it need to get the Request object as it's first parameter:
public function indexAction(Request $request)
{
$user = new User();
$form = $this->createForm(RegistrationType::class, $user);
$form->handleRequest($request);
if ($form->isValid()) {
// Do something here
}
}
See http://symfony.com/doc/current/book/forms.html#handling-form-submissions

Symfony 2 - separate form logic, show form errors after redirect

I want to separate form validation logic:
public function contactAction()
{
$form = $this->createForm(new ContactType());
$request = $this->get('request');
if ($request->isMethod('POST')) {
$form->submit($request);
if ($form->isValid()) {
$mailer = $this->get('mailer');
// .. setup a message and send it
return $this->redirect($this->generateUrl('_demo'));
}
}
return array('form' => $form->createView());
}
I want to translate into 2 separate actions:
public function contactAction()
{
$form = $this->createForm(new ContactType());
return array('form' => $form->createView());
}
public function contactSendAction()
{
$form = $this->createForm(new ContactType());
$request = $this->get('request');
if ($request->isMethod('POST')) {
$form->submit($request);
if ($form->isValid()) {
$mailer = $this->get('mailer');
// .. setup a message and send it using
return $this->redirect($this->generateUrl('_demo'));
}
}
// errors found - go back
return $this->redirect($this->generateUrl('contact'));
}
The problem is that when errors exist in the form - after form validation and redirect the do NOT showed in the contactAction. (probably they already will be forgotten after redirection - errors context will be lost)
If you check out how the code generated by the CRUD generator handles this you will see that a failed form validation does not return a redirect but instead uses the same view as the GET method. So in your example you would just:
return $this->render("YourBundle:Contact:contact.html.twig", array('form' => $form->createView()))
rather than return the redirect. This means you do not lose the form errors as you do in a redirect. Something else the CRUD generator adds is the Method requirement which means you could specify that the ContactSendAction requires the POST method and thus not need the extra if($request->isMethod('POST')){ statement.
You can also just return an array if you specify the template elsewhere, for example you could use the #Template annotation and then just
return array('form' => $form->createView())
This seems to work for me in Symfony 2.8:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller {
public function templateAction()
{
$form = $this->createForm(new MyFormType(), $myBoundInstance);
if ($session->has('previousRequest')) {
$form = $this->createForm(new MyFormType());
$form->handleRequest($session->get('previousRequest'));
$session->remove('previousRequest');
}
return array(
'form' => $form->createView(),
);
}
public function processingAction(Request $request)
{
$form = $this->createForm(new MyFormType(), $myBoundInstance);
$form->handleRequest($request);
if ($form->isValid()) {
// do some stuff
// ...
return redirectToNextPage();
}
$session->set('previousRequest', $request);
// handle errors
// ...
return redirectToPreviousPage();
}
}
Please note that redirectToNextPage and redirectToPreviousPage, as well as MyFormType, are pseudo code. You would have to replace these bits with your own logic.

Resources