Updating current Symfony User is attempting CREATE - symfony

Ive set up Symfony 4 to use a SAML login system. When a user is authenticated using SAML the UserFactory gets the User from the database if they already exist or persists and flushes a new user entity.
In a controller, Im then trying to use some simple code:
$user = $this->getUser(); // Returns an App\User\Entity object
$user->setFirstName('Test');
$this->em->persist($user);
$this->em->flush(); // Errors here
But Doctrine treats this user as a new entity, or throws an error that some parameters are missing.
I could just get the User again from the database, but that would mean doing it every time I wanted to use the current User object to do something.
$this->getDoctrine()->getRepository(User::class)->find($this->getUser()->getId())
The issue seems to be possibly related to the serialize and unserialize methods within the user class, as these attributes are the only ones available in the user object.
public function serialize()
{
return serialize(array(
$this->id,
$this->firstName,
$this->lastName,
$this->email,
$this->enabled,
$this->username,
));
}
public function unserialize($serialized)
{
list (
$this->id,
$this->firstName,
$this->lastName,
$this->email,
$this->enabled,
$this->username,
) = unserialize($serialized, ['allowed_classes' => false]
);
}
Once the user is authenticated, theyre stored in the session. The $this->getUser() is what is being returned form the Security Token, so Im confused as to why this would be different from a standard login authentication.
How can I ensure the full User object is available through $this->getUser()?

Even though your unserialized user has identifier it is unknown to Doctrine thus the insertion. To let Doctrine know about your entity you can use merge (please note it returns a new instance of the entity).

Related

symfony extra user info to be persisted in session after authentication

I am trying to use a property called lastLoginOnReference in my User entity. This property is not linked to any column in my corresponding db table as I don't want to store it.
I use this property in order to display to the user some activity logs since the last time he logs in.
I also have a lastLoginOn property, also in the User entity, which has a dedicated column in my user table.
what am I doing at authentication is this one (in my GoogleAuthenticator class):
public function getUser($credentials, UserProviderInterface $userProvider)
{
$googleUser = $this->getGoogleClient()
->fetchUserFromToken($credentials);
$email = $googleUser->getEmail();
$user = $this->em->getRepository(User::class)
->findOneBy(['email' => $email]);
if ($user) {
$user->setLastLoginOnReference($user->getLastLoginOn());
$user->setLastLoginOn(new DateTime('now'));
} else {
(not important)
}
$this->em->persist($user);
$this->em->flush();
return $user;
}
so I store in my lastLoginOnReference property the value from the lastLoginOn User property.
I update the lastLoginOn User property in the User repository and it works.
But when in my controller I use
$this->getUser();
getLastLoginOnReference returns null
I read the documentation and found out the user is apparently refreshed between every request. So I suspect my user object to be refreshed and as the lastLoginOnReference not in db, it is set to null
Then I have implemented the Serializable interface in my User entity, specifying this property to be serialized as well but it doesn't change anything.
login/logout are working as expected, and I am using Symfony 5.1

How to properply update an entity which contains an Image path?

I have a form that I use both for registration and edition of the user informations. This form contains a profile picture property on which I put #Assert\Image.
I succeed in creating a new user through my registration form but when I try to edit the user informations (with a PATCH method, just to update what need to be updated) I encounter an error with a 'File could not be found' message.
I suppose it's because the path stored in the database is a string and my #Assert\Image want an image.
I'm not sure about how I should manage this kind of update.
When I dd() the $user right after the submission, I see that the profilePicture property still contains the path saved in the database.
Here is my function regarding the form handling:
public function myProfile(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(UserFormType::class, $user, ['method' => 'PATCH']);
if ($request->isMethod('PATCH')){
$form->submit($request->request->get($form->getName()), false);
if ($form->isSubmitted() && $form->isValid()) {
//...
}
}
//if no request just display the page
return $this->render('connected/myProfile.html.twig', [
'user' => $user,
'userProfileForm' => $form->createView()
]);
}
The Validator will check if your object contains a image and that seems not the case when you’re updating your object.
A workaround is to use group validation you define a specific group to the property that have the assert Image and in the method getGroupSequence you return the group if you’re in creation (id == null) or if the property is setted.

Updating an entity through Doctrine while it's persisted by a backend process

I've got an issue with Doctrine being somehow nasty with automagical tracking of entities and changes. I've got a UserManager which gets data for a new user from a form and sends the data to the backend which creates the corresponding database entries. As the backend is only inserting some basic data like username, I want to persist everything else like a collection of user roles given through the form.
So my method should look like this:
public function create(User $user)
{
$this->createUserInBackend($user);
$this->em->merge($user);
$this->em->persist($user);
$this->em->flush();
}
My issue now is that Doctrine either drops the data from the given $user and replaces it with the database content. This way I lose the chosen user roles. Without the merge() Doctrine tries an INSERT which is clearly not what I want.
I tried everything coming to my mind from fetching a managed copy before the merge, cloning or whatever. In all cases the objects are linked so I lose the data from the form (although their spl_object_hash differ).
Some more simplified details as requested:
class User
{
// Username is tracked by backend
private $username;
// Fullname is only tracked by frontend/Doctrine
private $fullname;
}
Variant 1:
public function create(User $user)
{
// The user entity gets passed to the backend, which does some stuff
// and also inserts the entity in the database.
$this->createUserInBackend($user);
// The entitiy is in the database, but not managed by the EM.
// Therefore Doctrine does an INSERT.
$this->em->persist($user);
$this->em->flush();
}
Variant 2:
public function create(User $user)
{
// The user entity gets passed to the backend, which does some stuff
// and also inserts the entity in the database.
$this->createUserInBackend($user);
// Now there's an entity in the db with ID and username, but not the fullname
$user = $this->em->merge($user);
// The merge finds the entry in the database and refreshes its data.
// This leads to $user->fullname which was given in the form to be emptied. :(
$this->em->persist($user);
$this->em->flush();
}
I also tried to put the return value from merge into an $otherUser variable, but the objects are linked and fullname still gets dropped.
I'd just need something to tell Doctrine that the new entity is managed, but it should not touch its data. I've looked into the underlying UnitOfWork, but couldn't find a trick to solve this.

Ignore Password when user updates profile with FOSUserBundle

I am using FOSUserBundle and I am trying to create a page that allows a user to update their user profile. The problem I am facing is that my form does not require that the user reenter their password if they don't want to change/update their password. So when a user submits the form with an empty password the database will be updated with an empty string, and the user will not be able to log in.
How can I get my form to ignore updating the password field if it is not set? Below is the code I am using.
$user = $this->get('security.context')->getToken()->getUser();
//user form has email and repeating password fields
$userForm = $this->createForm(new UserFormType(), $user);
if ($request->getMethod() == 'POST') {
$userForm->bindRequest($request);
if($userForm->isValid()){
//this will be be empty string in the database if the user does not enter a password
$user->setPlainPassword($userForm->getData()->getPassword());
$em->flush();
}
}
I have tried a few things such as the following, but this is still empty because the bindRequest sets the empty password to the user
if($userForm->getData()->getPassword())
$user->setPlainPassword($userForm->getData()->getPassword());
I have also tried, but this results in a similar situation and causes an unneeded query
if($userForm->getData()->getPassword())
$user->setPlainPassword($userForm->getData()->getPassword());
else
$user->setPlainPassword($user->getPlainPassword());
Are there any elegant ways to handle this use case?
The problem is that you bind a form to a User Object before controls upon password.
Let's analyze your snippet of code.
Do the following
$user = $this->get('security.context')->getToken()->getUser();
will load an existing user into a User Object. Now you "build" a form with that data and if receive a post, you'll take the posted data into the previous object
$userForm = $this->createForm(new UserFormType(), $user);
if ($request->getMethod() == 'POST') {
$userForm->bindRequest($request);
So, onto bindRequest you have alredy lost previous password into the object (obviously not into database yet) if that was leave empty. Every control from now on is useless.
A solution in that case is to manually verify value of form's field directly into $request object before binding it to the underlying object.
You can do this with this simple snippet of code
$postedValues = $request->request->get('formName');
Now you have to verify that password value is filled
if($postedValues['plainPassword']) { ... }
where plainPassword I suppose to be the name of the field we're interesting in.
If you find that this field contain a value (else branch) you haven't to do anything.
Otherwise you have to retrieve original password from User Object and set it into $request corrisponding value.
(update) Otherwise you may retrieve password from User Object but since that password is stored with an hased valued, you can't put it into the $request object because it will suffer from hashing again.
What you could do - i suppose - is an array_pop directly into $request object and put away the field that messes all the things up (plainPassword)
Now that you had done those things, you can bind posted data to underlying object.
Another solution (maybe better because you move some business logic away from controller) is to use prePersist hook, but is more conceptually advanced. If you want to explore that solution, you can read this about form events
I think you should reconsider if this is in fact a good use case. Should users be able to edit other users passwords? At our institution we do not allow even the highest level admin to perform this task.
If a user needs their password changed we let them handle that themselves. If they have forgotten their password we allow them to retrieve it via email. If they need assistance with adjusting their email we allow our admins to assist users then. But all password updating and creation is done soley by the user.
I think it is great that FOSUserBundle makes it so difficult to do otherwise but if you must DonCallisto seems to have a good solution.
<?php
class User
{
public function setPassword($password)
{
if (false == empty($password)) {
$this->password = $password;
}
}
}
This will only update the password on the user if it isn't empty.
I have found a simple hack to get rid of the "Enter a password" form error.
Manualy set a dummy plainPassword in the user entity. After form validation just reset it before you flush the entity.
<?php
public function updateAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AppBundle:User')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Customer entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$postedValues = $request->request->get('appbundle_user');
/* HERE */ $entity->setPlainPassword('dummy'); // hack to avoid the "enter password" error
$editForm->handleRequest($request);
if ($editForm->isValid()) {
/* AND HERE */ $entity->setPlainPassword(''); // hack to avoid the "enter password" error
$em->flush();
return $this->redirect($this->generateUrl('customer_edit', array('id' => $id)));
}
return array(
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
);
}

How to update a Doctrine Entity from a serialized JSON?

We are using Symfony2 to create an API. When updating a record, we expect the JSON input to represent a serialized updated entity. The JSON data will not contain some fields (for instance, CreatedAt should be set only once when the entity is created - and never updated). For instance, here is an example JSON PUT request:
{"id":"1","name":"anyname","description":"anydescription"}
Here is the PHP code on the Controller that should update the entity according to the JSON above (we are using JMS serializer Bundle):
$supplier = $serializer->deserialize(
$this->get('request')->getContent(),
'WhateverEntity',
'json'
);
The EntityManger understands (correctly) that this is an update request (in fact, a SELECT query is implicitly triggered). The EntityManager also guess (not correctly) that CreatedAt property should be NULLified - it should instead keep the previous one.
How to fix this issue?
It's possible as well to do it with Symfony Serializer using object_to_populate option.
Example: I receive JSON request. If record exists in database I want to update fields received in body, if it does not exist I want to create new one.
/**
* #Route("/{id}", methods={"PUT"})
*/
public function upsert(string $id, Request $request, SerializerInterface $serializer)
{
$content = $request->getContent(); // Get json from request
$product = $this->getDoctrine()->getRepository(Product::class)->findOne($id); // Try to find product in database with provided id
if (!$product) { // If product does not exist, create fresh entity
$product = new Product();
}
$product = $serializer->deserialize(
$content,
Product::class,
'json',
['object_to_populate' => $product] // Populate deserialized JSON content into existing/new entity
);
// validation, etc...
$this->getDoctrine()->getManager()->persist($product); // Will produce update/instert statement
$this->getDoctrine()->getManager()->flush($product);
// (...)
using the JMSSerializerBundle follow the install instructions at
http://jmsyst.com/bundles/JMSSerializerBundle
either create your own serializer service or alter the JMSSerializerBundle to use the doctrine object constructor instead of the simple object constructor.
<service id="jms_serializer.object_constructor" alias="jms_serializer.doctrine_object_constructor" public="false"/>
This basically handles exactly what Ocramius solution does but using the JMSSerializerBundles deserialize.
I would use the Doctrine\ORM\Mapping\ClassMetadata API to discover existing fields in your entity.
You can do following (I don't know how JMSSerializerBundle works):
//Unserialize data into $data
$metadata = $em->getMetadataFactory()->getMetadataFor($FQCN);
$id = array();
foreach ($metadata->getIdentifierFieldNames() as $identifier) {
if (!isset($data[$identifier])) {
throw new InvalidArgumentException('Missing identifier');
}
$id[$identifier] = $data[$identifier];
unset($data[$identifier]);
}
$entity = $em->find($metadata->getName(), $id);
foreach ($metadata->getFieldNames() as $field) {
//add necessary checks about field read/write operation feasibility here
if (isset($data[$field])) {
//careful! setters are not being called! Inflection is up to you if you need it!
$metadata->setFieldValue($entity, $field, $data[$field]);
}
}
$em->flush();

Resources