Entity persists even when form has error - php

I have an issue where I have a form type that persists the associated entity even when the form is not valid.
I have confirmed that the form indeed has errors via $form->getErrorsAsString(). I have also confirmed that the logical if statement that checks if the form is valid or not comes out false. The entity still persists despite the fact that the form is never valid.
I'm not sure what I'm doing wrong here as I have no other spot that I can find that either persists the entity or flushes the entity manager. Here's my controller:
/**
* #Route("/settings/profile", name="settings_profile")
* #Template();
*/
public function profileAction()
{
$user = $this->getUser();
$profile = $user->getUserProfile();
if (null === $profile) {
$profile = new UserProfile();
$profile->setUser($user);
$profileDataModel = $profile;
} else {
$profileDataModel = $this->getDoctrine()->getManager()->find('MyAppBundle:UserProfile',$profile->getId());
}
$form = $this->createForm(new ProfileType(),$profileDataModel);
$request = $this->getRequest();
if ($request->getMethod() === 'POST') {
$form->bind($request);
if ($form->isValid()) {
// This logic never gets executed!
$em = $this->getDoctrine()->getManager();
$profile = $form->getData();
$em->persist($profile);
$em->flush();
$this->get('session')->setFlash('profile_saved', 'Your profile was saved.');
return $this->redirect($this->generateUrl('settings_profile'));
}
}
return array(
'form' => $form->createView(),
);
}

I must have a listener or something somewhere that is persisting the user.
My work around for this temporarily is to do:
$em = $this->getDoctrine()->getManager()
if ($form->isValid()) {
// persist
} else {
$em->clear();
}
Until I can ferret out what listener or other data transformer is causing this.

Related

Adding related entities better way

How to implement adding related Role entities better way? It would be nice to keep it inside of a form if possible or maybe if I add a method to an User entity directly... Is there built-in mechanism for it or what patter shall I use?
public function add(Request $request)
{
$data = $request->request->all();
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->submit($data);
if (false === $form->isValid()) {
return $form;
}
$user = $form->getData();
// Adding a Roles
if (!empty($data['user_roles'])) {
foreach ($data['user_roles'] as $value) {
// Checking if a Role is present in DB
$role = $this->getDoctrine()
->getRepository(Role::class)
->findOneBy(['role_name' => $value]);
if ($role) {
$user->setUserRole($role);
}
}
}
$this->em->persist($user);
$this->em->flush();
return new JsonResponse(
[
'status' => 'ok',
'last_insert_id' => $user->getId(),
],
JsonResponse::HTTP_CREATED
);
}
I've always worked like that and I've never had a problem. In fact, I don't even use symfony's forms because they're not that flexible. The best way to do it is the one that you feel more comfortable with. Just be sure to be clean and consistent.
Of course you can add a method to the User entity, but it's just a matter of modularization, that's not very necessary in this case.

Symfony 4 Validator - Auto logout if invalid

I wanted to validate my User Object ($user) with the Symfony Validator and return a JsonResponse ($response) if the form input data is valid / is not valid.
But I have the issue that I get logged out automatically when the data could not be validated. I have to login again and this is not the behaviour I expect when some data ist not valid. I found a workaround (see comments below) but this is not very satisfying :/
Here is the method of my Controller:
/**
* Update user profile data
*
* #Route("/api/users/updateprofile")
* #Security("is_granted('USERS_LIST')")
*/
public function apiProfileUpdate(ValidatorInterface $validator, FlashMessageBuilder $flashMessageBuilder)
{
$request = Request::createFromGlobals();
// Prepare Response
$response = new JsonResponse();
$response->setData([]);
/** #var User $user */
$user = $this->getUser();
$oldName = $user->getName();
$oldEmail = $user->getEmail();
$user->setName($request->request->get('name'));
$user->setEmail($request->request->get('email'));
$errors = $validator->validate($user);
if (count($errors) > 0) { // if this -> auto logout
$user->setName($oldName); // if I set the both attributes back to the old value
$user->setEmail($oldEmail); // then I don't get logged out automatically but this is just a workaround and not satisfying
$entityManager = $this->getDoctrine()->getManager(); // forgot to remove this
$entityManager->persist($user); // and this line, this is actually deleted in the real code
foreach ($errors as $error) {
$errorMessage = $error->getMessage();
$errorField = $error->getPropertyPath();
$flashMessageBuilder->addErrorMessage($errorMessage, $errorField);
};
return $response;
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$flashMessageBuilder->addSuccessMessage("Success!");
return $response;
}
Sorry for my bad english and thank you in advance!
You are persisting the User Object even if there are errors in validation, which can cause the problem with the logout.
Try to update ONLY in case there is no validation error:
public function apiProfileUpdate(ValidatorInterface $validator, FlashMessageBuilder $flashMessageBuilder)
{
$request = Request::createFromGlobals();
// Prepare Response
$response = new JsonResponse();
$response->setData([]);
/** #var User $user */
$user = $this->getUser();
$user->setName($request->request->get('name'));
$user->setEmail($request->request->get('email'));
$errors = $validator->validate($user);
if (count($errors) == 0) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$flashMessageBuilder->addSuccessMessage("Success!");
return $response;
}
foreach ($errors as $error) {
$errorMessage = $error->getMessage();
$errorField = $error->getPropertyPath();
$flashMessageBuilder->addErrorMessage($errorMessage, $errorField);
};
return $response;
}
But I think you should not mix API calls and the classical form approach utilizing FlashMessages, instead return a proper JSON result.
So consider to change the code accordingly:
public function apiProfileUpdate(ValidatorInterface $validator, FlashMessageBuilder $flashMessageBuilder)
{
$request = Request::createFromGlobals();
/** #var User $user */
$user = $this->getUser();
$user->setName($request->request->get('name'));
$user->setEmail($request->request->get('email'));
$errors = $validator->validate($user);
if (count($errors) == 0) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
return new JsonResponse(['success' => true]);
}
$data = [];
foreach ($errors as $error) {
$data[$error->getPropertyPath()] = $error->getMessage();
};
return new JsonResponse(['success' => false, 'errors' => $data], 400);
}
Now your calling code can handle a 200 result (success) and the error case with status code 400 and display the error messages for all failed fields from the errors part in the result body.

Symfony / phpUnit : functional test of a form modifying an entity

How would you do a functional test (not unit tests) of a form binded to an entity ?
Context
Let's say you have an entity "Car", with a field "id" and another field "numberPlate", and a page to edit data about a car.
CarController.php :
//...
public function imsiDetailsChangeAction(Request $request)
{
$car_id = $request->get('car_id');
$car = $this->getDoctrine()->getRepository('ClnGsmBundle:Car')->Find($car_id);
if ($simCard != null)
{
$form = $this->createForm(new CarType()), $car);
if($request->isMethod('POST'))
{
$form->bind($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->flush();
return $this->redirect($this->generateUrl('car_view', array('car_id' => $car->getId())));
}
}
}
else
{
throw new NotFoundHttpException();
}
return $this->render('SiteBundle:Car:carEdit.html.twig', array('car' => $car, 'form' => $form->createView()));
}
//...
What I want
A test using phpUnit doing the following :
create a Car entity with the numberPlate "QWE-456"
load the page with the form
using the crawler, replace the numberPlate with "AZE-123" in the form, and submit the form
assert that my car entity's numberPlate now equals "AZE-123"
What I tried
(just in case: my own code is a bit different, here is what I would do with the car example)
CarControllerTest.php :
//...
public function SetUp()
{
//start kernel, stores entity manager in $this->em and client in $this->client
}
//...
public function testEditForm()
{
$car = new Car();
$car->setNumberPlate("QWE-456");
$this->entityManager->persist($simCard);
$this->em->flush();
$crawler = $this->client->request('GET', '/fr/Car/edit/'.$car->getId());
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$formNode = $crawler->filterXpath("//div[#id='main']//form");
$form = $formNode->form(array(
'car[plateNumber]'=>'AZE-123',
));
//var_dump($car->getPlateNumber());
$this->client->submit($form);
//var_dump($car->getPlateNumber());
$this->assertEquals('AZE-123',$car->getPlateNumber);
}
I expect this test to pass, and the second var_dump to print "AZE-123" instead of "QWE-456". But my entity isn't modified.
How should I do this ?
You should refresh the data reloading it from the database: the refresh method do it for you, so try this:
$this->client->submit($form);
$this->em->refresh($car);
$this->assertEquals('AZE-123',$car->getPlateNumber);
I suggest you to check before the HTTP Response in order to verify the correct interaction, as example:
$response = $this->client->getResponse();
$this->assertTrue($response->isRedirection());
Hope this help

Symfony 3, populating token and refreshing user

repository with issue
I have a form for entity User with email field:
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(),
new Email([
'checkMX' => true,
])
],
'required' => true
])
when i'm editing email to something like test#gmail.com1 and submit form it shows me error "This value is not a valid email address." THat's ok, but after that symfony populate wrong email into token and when i'm going to any other page or just reload page, i'm getting this:
WARNING security Username could not be found in the selected user
provider.
i think question is: why symfony populate wrong Email that failed validation into token and how i could prevent it?
controller:
public function meSettingsAction(Request $request)
{
$user = $this->getUser();
$userUnSubscribed = $this->getDoctrine()->getRepository('AppBundle:UserUnsubs')->findOneBy(
[
'email' => $user->getEmail(),
]
);
$form = $this->createForm(UserSettingsType::class, $user);
$form->get('subscribed')->setData(!(bool)$userUnSubscribed);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/**
* #var $user User
*/
$user = $form->getData();
/** #var UploadedFile $avatar */
$avatar = $request->files->get('user_settings')['photo'];
$em = $this->getDoctrine()->getManager();
if ($avatar) {
$avatar_content = file_get_contents($avatar->getRealPath());
$avatarName = uniqid().'.jpg';
$oldAvatar = $user->getPhoto();
$user
->setState(User::PHOTO_STATE_UNCHECKED)
->setPhoto($avatarName);
$gearmanClient = $this->get('gearman.client');
$gearmanClient->doBackgroundDependsOnEnv(
'avatar_content_upload',
serialize(['content' => $avatar_content, 'avatarName' => $avatarName, 'oldAvatar' => $oldAvatar])
);
}
$subscribed = $form->get('subscribed')->getData();
if ((bool)$userUnSubscribed && $subscribed) {
$em->remove($userUnSubscribed);
} elseif (!(bool)$userUnSubscribed && !$subscribed) {
$userUnSubscribed = new UserUnsubs();
$userUnSubscribed->setEmail($form->get('email')->getData())->setTs(time());
$em->persist($userUnSubscribed);
}
$user->setLastTs(time());
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
$this->get('user.manager')->refresh($user);
return $this->redirectToRoute('me');
}
return $this->render(
':user:settings.html.twig',
[
'form' => $form->createView(),
]
);
}
UPD:
it works fine if i change in OAuthProvider:
/**
* #param \Symfony\Component\Security\Core\User\UserInterface $user
*
* #return \Symfony\Component\Security\Core\User\UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getName());
}
to:
/**
* #param \Symfony\Component\Security\Core\User\UserInterface $user
*
* #return \Symfony\Component\Security\Core\User\UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $this->userManager($user->getId());
}
but it seems to be dirty hack.
Thanks.
Your user token seems to be updated by the form, even if the email constraint stop the flush.
Can you check if your form past the isValid function ?
You can maybe try to avoid it with an event listener or a validator.
With an event SUBMIT you should be able to check the email integrity, and then add a FormError to avoid the refreshUser.
This is a tricky one, thanks to the repository it was easier to isolate the problem. You are binding the user object form the authentication token to the createForm() method. After the
$form->handleRequest($request)
call the email off the token user object is updated.
I first thought to solve this by implementing the EquatableInterface.html in the User entity but this did not work, as the compared object already had the wrong email address set.
It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user. This interface requires an isEqualTo() method.)
Than I thought about forcing a reload of the user from the db and resetting the security token, but in the it came to my mind, that it might be sufficient to just refresh the current user object from the database in case the form fails:
$this->get('doctrine')->getManager()->refresh($this->getUser());`
In your controller, this would solve your issue.
/**
* #Route("/edit_me", name="edit")
* #Security("has_role('ROLE_USER')")
*/
public function editMyselfAction(Request $request) {
$form = $this->createForm(User::class, $this->getUser());
if ($request->isMethod(Request::METHOD_POST)) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
} else {
$this->get('doctrine')->getManager()->refresh($this->getUser());
}
}
return $this->render(':security:edit.html.twig',['form' => $form->createView()]);
}
Alternative solution
The issue at the Symfony repository resulted in some valuable input about Avoiding Entities in Forms and
Decoupling Your Security User which provides a more complex approach for a solution to your problem.

Symfony 2 - Entity has to be managed or scheduled for removal for single computation

When I am submitting symfony2 form I got the following error:
Entity has to be managed or scheduled for removal for single computation
What does this error mean?
I am using the form which is aimed at adding new item to DB. I have multiple ManyToOne relations in the form.
/**
* This code is aimed at checking if the book is choseen and therefore whether any further works may be carried out
*/
$session = new Session();
if(!$session->get("App_Books_Chosen_Lp")) return new RedirectResponse($this->generateUrl('app_listbooks'));
$request = $this->get('request');
$em = $this->getDoctrine()->getManager();
if($item_id != null)
{
/* THIS CODE IS NOT EXECUTED IN THE GIVEN CASE */
}
else
{
// Add
$item = new Items();
$form = $this->createForm(new ItemsType(), $item);
$form->add('save', 'submit', array('label' => 'Add item'));
}
$form->remove('documentid');
$form->remove('book');
$form->handleRequest($request);
if ($form->isValid()) {
if($item_id != null)
{
/* THIS CODE IS NOT EXECUTED IN THE GIVEN CASE */
}
else
{
/* HERE ERROR OCCURS */
// Add
$book = $em->getReference('AppBundle:Books', $session->get("App_Books_Chosen_Lp"));
if( $book ) $item->setBook($book);
$doc = $em->getReference('AppBundle:Documents', $doc_id);
if( $doc ) $item->setDocumentid($doc);
$em->flush($item);
$session = new session();
$session->getFlashBag()->add('msg', 'Item was added.');
$url = $this->generateUrl("app_documents_details", array("id" => $doc_id));
return $this->redirect($url);
}
You need to persist your entity to let the EntityManager knows that it exists.
$em->persist($item);
$em->flush($item);

Categories