Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Data Persister: Encoding the Plain Password

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When an API client makes a POST request to /api/users, we need to be able to run some code after API Platform deserializes the JSON into a User object, but before it gets saved to Doctrine. That code will encode the plainPassword and set it on the password property.

Introducing Data Persisters

How can we do that? One great answer is a custom "data persister". OooooOOOo. API Platform comes with only one data persister out-of-the-box, at least, only one that we care about for now: the Doctrine data persister. After deserializing the data into a User object, running security checks and executing validation, API Platform finally says:

It's time to save this resource!

To figure out how to save the object, it loops over all of its data persisters... so... really... just one at this point... and asks:

Hi data persister! Do you know how to "save" this object?

Because our two API resources - User and CheeseListing are both Doctrine entities, the Doctrine data persister says:

Oh yea, I totally do know how to save that!

And then it happily calls persist() and flush() on the entity manager.

This... is awesome. Why? Because if you want to hook into the "saving" process... or if you ever create an API Resource class that is not stored in Doctrine, you can do that beautifully with a custom data persister.

Check it out: in the src/ directory - it doesn't matter where - but let's create a DataPersister/ directory with a new class inside: UserDataPersister.

This class will be responsible for "persisting" User objects. Make it implement DataPersisterInterface. You could also use ContextAwareDataPersisterInterface... which is the same, except that all 3 methods are passed the "context", in case you need the $context to help your logic.

... lines 1 - 4
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
class UserDataPersister implements DataPersisterInterface
{
... lines 9 - 22
}

Anyways I'll go to the Code -> Generate menu - or Command+N on a Mac - and select "Implement Methods" to generate the three methods this interface requires.

... lines 1 - 8
public function supports($data): bool
{
// TODO: Implement supports() method.
}
public function persist($data)
{
// TODO: Implement persist() method.
}
public function remove($data)
{
// TODO: Implement remove() method.
}
... lines 23 - 24

And... we're... ready! As soon as you create a class that implements DataPersisterInterface, API Platform will immediately start using that. This means that, whenever an object is saved - or removed - it will now call supports() on our data persister to see if we know how to handle it.

In our case, if data is a User object, we do support saving this object. Say that with: return $data instanceof User.

... lines 1 - 17
public function supports($data): bool
{
return $data instanceof User;
}
... lines 22 - 35

As soon as API Platform finds one data persister whose supports() returns true, it calls persist() on that data persister and does not call any other data persisters. The core "Doctrine" data persister we talked about earlier has a really low "priority" in this system and so its supports() method is always called last. That means that our custom data persister is now solely responsible for saving User objects, but the core Doctrine data persister will still handle all other Doctrine entities.

Saving in the Data Persister

Ok, forget about encoding the password for a minute. Now that our class is completely responsible for saving users... we need to... yea know... make sure we save the user! We need to call persist and flush on the entity manager.

Add public function __construct() with the EntityManagerInterface $entityManager argument to autowire that into our class. I'll hit my favorite Alt + Enter and select "Initialize fields" to create that property and set it.

... lines 1 - 6
use Doctrine\ORM\EntityManagerInterface;
... line 8
class UserDataPersister implements DataPersisterInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
... lines 17 - 33
}

Down in persist(), it's pretty simple: $this->entityManager->persist($data) and $this->entityManager->flush(). Data persisters are also called when an object is being deleted. In remove(), we need $this->entityManager->remove($data) and $this->entityManager->flush().

... lines 1 - 22
public function persist($data)
{
$this->entityManager->persist($data);
$this->entityManager->flush();
}
public function remove($data)
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
... lines 34 - 35

Congrats! We now have a data persister that... does exactly the same thing as the core Doctrine data persister! But... oh yea... now, we're dangerous. Now we can encode the plain password.

Encoding the Plain Password

To do that, we need to autowire the service responsible for encoding passwords. If you can't remember the right type-hint, find your terminal and run:

php bin/console debug:autowiring pass

And... there it is: UserPasswordEncoderInterface. Add the argument - UserPasswordEncoderInterface $userPasswordEncoder - hit "Alt + Enter" again and select "Initialize fields" to create that property and set it.

... lines 1 - 7
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
... line 9
class UserDataPersister implements DataPersisterInterface
{
... line 12
private $userPasswordEncoder;
... line 14
public function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $userPasswordEncoder)
{
... line 17
$this->userPasswordEncoder = $userPasswordEncoder;
}
... lines 20 - 46
}

Now, down in persist(), we know that $data will always be an instance of User. ... because that's the only time our supports() method returns true. I'm going to add a little PHPdoc above this to help my editor.

Hey PhpStorm! $data is a User! Ok?,

... lines 1 - 5
use App\Entity\User;
... lines 7 - 9
class UserDataPersister implements DataPersisterInterface
{
... lines 12 - 25
/**
* @param User $data
*/
public function persist($data)
... lines 30 - 46
}

Let's think. This endpoint will be called both when creating a user, but also when it's being updated. And... when someone updates a User record, they may or may not send the plainPassword field in the PUT data. They would probably only send this if they wanted to update the password.

This means that the plainPassword field might be blank here. And if it is, we should do nothing. So, if $data->getPlainPassword(), then $data->setPassword() to $this->userPasswordEncoder->encodePassword() passing the User object - that's $data - and the plain password: $data->getPlainPassword().

That's it friends! Well, to be extra cool, let's call $data->eraseCredentials()... just to make sure the plain password doesn't stick around any longer than it needs to. Again, this is probably not needed because this field isn't saved to the database anyways... but it might avoid the plainPassword from being serialized to the session via the security system.

... lines 1 - 28
public function persist($data)
{
if ($data->getPlainPassword()) {
$data->setPassword(
$this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
);
$data->eraseCredentials();
}
... lines 37 - 39
}
... lines 41 - 48

And... done! Aren't data persisters positively lovely?

Oh, well, we're not quite finished yet. The field in our API is still called plainPassword... but we wrote our test expecting that it would be called just password... which I kinda like better.

No problem. Inside User, find the plainPassword property and give it a new identity: @SerializedName("password").

... lines 1 - 13
use Symfony\Component\Serializer\Annotation\SerializedName;
... lines 15 - 36
class User implements UserInterface
{
... lines 39 - 78
/**
... line 80
* @SerializedName("password")
*/
private $plainPassword;
... lines 84 - 216
}

Let's check that on the docs... under the POST operation... perfect!

So... how can we see if this all works? Oh... I don't know... maybe we can run our awesome test!

php bin/phpunit --filter=testCreateUser

Above all the noise.. we got it!

Next, our validation rules around the plainPassword field... aren't quite right yet. And it's trickier than it looks at first: plainPassword should be required when creating a User, but not when updating it. Duh, duh, duh!

Leave a comment!

82
Login or Register to join the conversation
Willems Avatar
Willems Avatar Willems | posted 3 years ago

Hi SymfonyCast team,

I was wondering why you did not use an event instead of data persister to hook and crypt password (eg : PRE_WRITE event) ?

Regards

2 Reply

Hey Willems!

Good question :). And I didn't have any specific reason behind this. The truth is that data persisters were the way that occurred to me first - then later I realized that an event listener can also accomplish this. I can't think of any practical difference between these two options. As a side note, I *had* planned to show events in a future tutorial - mentioning PRE_WRITE as a parallel option to a data persister might be a good thing to mention there.

Cheers!

Reply

Hi again weaverryan!

Actually, I'm going to modify my statement and say that there IS at least one difference between using a data persister versus the PRE_WRITE event. A data persister will work both for the REST API *and* also if you start using GraphQL with API Platform. The PRE_WRITE event, however, is specific to the REST API. So, the data persister is a bit more powerful because it's used in more places :).

Cheers!

Reply

There's a situation where a Doctrine event on PrePersist might not work with GraphQL? The data persister isn't triggered by my fixtures for my tests so, here's another difference I guess.

Reply

Hey @Jean-Nicolas Lagneau!

Hmm. No, there should be no difference when it comes to Doctrine event and GraphQL. But REST and GraphQL modify objects and then save them via Doctrine. And this should trigger the PrePersist identically. Now, while I'm pretty sure I'm right, full disclosure, I have not tried this in GraphQL, so I could be wrong.

The most common reason for this part to "go wrong" is that no "persisted properties" are modified on your User object. And so, when it's saved, Doctrine is "smart enough" to realize that no persisted properties were changed, and so it skips saving. That can happen if, for example, you make a GraphQL request to update the "password" property *only*. The problem would be that this updates the plainPassword property, which is not a persisted property... and so then Doctrine *thinks* that nothing needs to save. If you have this situation, you need to modify some persisted property from inside of setPlainPassword(). An easy thing to do is have an updatedAt field where you set it in that method:


public function setPlainPassword(string $password)
{
$this->plainPassword = $password;
// this will trigger the full save cycle to happen
$this->updatedAt = new \DateTime();
}

Let me know if that helps!

Cheers!

Reply
Startchurch Avatar

I saw this after I asked my question

Reply
Evozon S. Avatar
Evozon S. Avatar Evozon S. | posted 1 year ago

Hello

I'm leaving some alternative extension points just in case.
https://api-platform.com/do...
https://api-platform.com/do...

1 Reply

Hey Victor,

Thanks for sharing some useful links to the official docs, it might help to understand concepts better from a different point of view :)

Cheers!

Reply

I'm wondering why you chose not to decorate the default persister, but instead implement yours fully. Decorating seems like a cleaner choice to me, plus it allows you to be decoupled from the actual storage, don't you think?

Reply

Hey Rimas,

Good question! Fairly speaking, I'm not sure 100%, probably Ryan wanted to show the flexibility here, that's why a completely custom persister would be more flexible and probably simpler from the implementation point of view. But decorating the default persister sounds cool to me too, good catch! And agree, it would be a cleaner way IMO too. Feel free to do this way instead :)

Cheers!

Reply

Well, as I progress throgh this course, my question just got answered: Ryan rewrites this persister in Part 3 Chapters 2 and 3 to use the decorator pattern. 🙃

So you could say I'm ahead of schedule. Aren't I cool! 😎

Reply

Hey Rimas,

Ah, you're definitely cool! ;) Yeah, that's great you were able to see the potential of using decorator pattern in advance, well done! :)

Cheers!

Reply
Daniel U. Avatar
Daniel U. Avatar Daniel U. | posted 1 year ago

Hi Team,

I was wondering what's the standard for the custom data persister is we don't want to allow to remove an object. Do we return an exception in the remove function?

Regards

Reply

Hey Daniel,

I think you can do that but if something is calling the remove method of your data persister when it should not, it means you have a problem else where. What I mean is if you feel like something or someone can call that method without your consent, then, just throw an exception right there but if that's not the case, disabling the remove method for such ApiResource should be good enough

Cheers!

Reply
Default user avatar
Default user avatar Jascha Lukas Gugat | posted 1 year ago

Hi symfony Team,
i got a little stuck on combining the previously used embedded write functionality with contextawaredatapersisters.

i am using a mapped superclass called baseEntity with properties all of my entities should have like uuid, createdBy, createdAt etc. The values for these properties are generated by using the baseEntites datapersister and it works fine if i am posting for example a bookEntity which extends the baseEntity. It is also working for a bookCommentEntity extending also the baseEntity and mapped to the bookEntity. But if i am using the embedded write functionality to directly create a bookComment on posting a new book with cascade persist and denormalization groups the bookComment is created successfully but without the generated uuid, createdBy, and createdAt values. The baseEnties datapersister is simply not called for the commentEntities that are generated by the embedded write cascade. Is there something miss-configured or is this behaviour expected for a datapersister and if so how can i ensure that the datapersister is also applied to the creation process of these entities?

Regards

Reply

Hi Jascha Lukas Gugat!

> The baseEnties datapersister is simply not called for the commentEntities that are generated by the embedded write cascade. Is there something miss-configured or is this behaviour expected for a datapersister and if so how can i ensure that the datapersister is also applied to the creation process of these entities?

No, you nailed it: a custom data persister (and the same is true for data providers) is only called for the main, top-level resource. It makes sense when you think about if from API Platform's perspective: at the end of the request, API Platform says "Someone save this object for me!". And then the data persister system takes over. But if that object has embedded objects, that is entirely up to whatever data persister is handling the object to deal with (e.g. the Doctrine data persister of course handles saving embedded objects).

My advice would be to actually set these properties a different way. This is... motivated in part by "how can we solve this problem" but it's also motivated by the fact that low-level fields (like uuid, createdAt and createdBy) feel more appropriate to me as actual Doctrine listeners. The big reason is that I want these set 100% of the time, regardless of whether an entity is being created through my API, via custom code, via a custom console command, etc.

So, that's my advice: solve this with a Doctrine event listener :).

I hope this helps! Cheers!

Reply
Petru L. Avatar
Petru L. Avatar Petru L. | posted 1 year ago | edited

Hey there , i got a name field with @Assert/NotBlank which gets triggered before the DataPersister, causing: "name: This value should not be null.". Adding DisableAutoMapping in the annotation made no difference. Any idea how can i avoid this and still keep the constraint?

Reply

Hey Petru L.!

Hmm. Yea, you should remove the @Assert\NotBlank :). That may sound wrong at first. But if you are not expecting your user to send this field (and you are going to fill it in yourself in a data persister), then from the user's perspective, it is *not* a required field. If you want this field to be required sometimes (e.g. on EDIT) but not others (e.g. CREATE), then you can use validation groups for that.

Let me know how that fits your situation.

Cheers!

Reply
Pedro S. Avatar
Pedro S. Avatar Pedro S. | posted 1 year ago

Hello Everyone!

I'm facing a problem where I've been stuck for a while. I have created a Data Persister that is working fine when I'm running a test making a post request, but it seems that the Data Persister is not even called when I try to make a post request from the browser (or Postman). Any idea what could be wrong?

Many thanks in advance

Reply

Hey Pedro S.

Could you double check the support() method returns true when it should. And, also check that your DataPersister implements the ApiPlatform\Core\DataPersister\DataPersisterInterface interface

Cheers!

Reply
Zongor A. Avatar
Zongor A. Avatar Zongor A. | posted 1 year ago

Hi! How I can update multiple data. I want send objects in collection with PUT, it is possible? (Not working for me)

Reply

Hey Zongor A.

Unfortunately it's not possible by default as I know... but you can use custom operations or to achieve what you need. https://api-platform.com/do...

Cheers!

Reply
triemli Avatar
triemli Avatar triemli | posted 2 years ago

Hi colleagues,
I'm actually stuck with repeated validator.

In the documentation I found this example:

$builder->add('password', RepeatedType::class, [
'type' => PasswordType::class,
'invalid_message' => 'The password fields must match.',
'options' => ['attr' => ['class' => 'password-field']],
'required' => true,
'first_options' => ['label' => 'Password'],
'second_options' => ['label' => 'Repeat Password'],
]);

Upon a successful form submit, the value entered into both of the “password” fields becomes the data of the password key.

both of the “password” but where is the second fieldname?

I found in the sources of RepeatedType:

public function getBlockPrefix()
{
return 'repeated';
}

But I send any variants of 'repeated_password', 'repeatedPassword' it doesn't work for me.

For more I get an error validation: "This form should not contain extra fields." Ok let's add:

 ->add('repeated_password', PasswordType::class, [
'mapped' => false,
])

$form->isValid() //false
Validation false. No errors. Nothing.

I tried to define first_name and second_name properties, but this both goes to "This form should not contain extra fields."

How are you validate password === password_repeated?

Reply

Hey WebAdequate,

This should be validated out of the box when you call "$form->isValid()" in your controller. Why do you want to get the value from that second field? In theory, you don't need it. The $form->isValid() will return true if both password fields match and false if they are not. And *if* they match - the value from the first field will be enough for you, as its' the same as the value from the 2nd field :)

Yes, by default if you add some extra fields to your form - the validation will fail with that "This form should not contain extra fields." error message. Check your HTML form for any custom fields you created manually. Just use form_row('field_name') to render form fields you have in your form type, don't add any extra HTML fields manually.

Cheers!

Reply
triemli Avatar

I didn't use twig form with their magic becuase I use Vue. Vue knows noithing about our twig. I just collect data with vue and sent it with axios. basically i have fields:

username,
password,
and ... unknown username field, repeated_password or maybe _repeated_password?.

User must enter the password two times.

Reply

Hey WebAdequate,

Ah, I see... This is even easier then! Just open Chrome Developer toolbar and inspect the code of your form - you will see all your fields in it and then you will just need to get them in your JS code to be able to get their values and validation.

Well, you can still do validation on server side, e.g. send an AJAX request with the data to the server and use Symfony validator to validate the data. Then return the response saying whether the validation is passed or an array of errors to show to the user using Vue JS.

Anyway, validating such forms on server side is a good idea as it's more secure.

I hope this helps!

Cheers!

Reply
Benjamin K. Avatar
Benjamin K. Avatar Benjamin K. | posted 2 years ago

After a Problem with the DataPersister i noticed my generated ID Getter Method from maker Bundle is ?int. But an ID cant be null or? So make it sense to remove the question mark and after i get a better failure message?
Instead: Unable to generate an IRI for "App\Entity\Checklist". i get then: Return value of App\Entity\Checklist::getId() must be of the type int, null returned


public function getId(): ?int
{
return $this->id;
}

Reply

Hey Benjamin K.

I guess it's wrong error, because ID can be null if your entity is not save yet so probably you forgot to persist() or flush() somewhere in your persister. Check full class code on one the latest codeblocks from script page

Cheers!

Reply
Benjamin K. Avatar
Benjamin K. Avatar Benjamin K. | posted 2 years ago

Hello SymfonyCast Team
i have a problem with the Data persister. its brokes post operation for all entities if i write in service yaml the decorator. The field ID is null and looks like no auto ID is created then anymore. Did i miss something?

App\DataPersister\UserDataPersister:
decorates: 'api_platform.doctrine.orm.data_persister'

ID is too null when remove the upper code but then it works.

Dumped Contents
In UserDataPersister.php line 26:
Checklist {#1189 ▼
-id: null
...
}


Exceptions
Unable to generate an IRI for "App\Entity\Checklist".



[1/2] InvalidParameterException
Symfony\Component\Routing\Exception\InvalidParameterException:
Parameter "id" for route "api_checklists_get_item" must match "[^/\.]++" ("" given) to generate a corresponding URL.

Reply
Ian D. Avatar

I'm using api-platform/core 2.5.6 and symfony/framework-bundle 5.0. It appears that the Symfony validators are firing based on the ORM definition of user before the data persister is used. The error I get when I follow this tutorial is

{
"@context": "/api/contexts/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "password: This value should not be null.",
"violations": [
{
"propertyPath": "password",
"message": "This value should not be null."
}
]
}

The code in my User entity is
/**
* @Groups({"user:write"})
* @ORM\Column(type="string")
*/
private ?string $password;

/**
* @Groups({"user:write"})
* @SerializedName("password")
*/
private ?string $plainPassword;

I have no further annotations on setPlainPassword or setPassword.

Any ideas? Suggestions?

Reply

Hey Ian D.!

Is it possible that you have validation auto-mapping enabled? That is where validation rules are "guessed" from Doctrine mapping information (and other sources) and this is a common problem it causes: it adds a NotNull constraints to your password field. If you do have it enabled, you'll need to add @Assert\DisableAutomapping() above the password field like we do here: https://symfonycasts.com/sc...

Cheers!

Reply
Mario J. Avatar

I've struggled with this for days and also the people in the symfony-dev slack couldn't help me. I dd'd in the data persister but it seemed that is was never called which made debugging quite hard. After days I came back here and the answer was the solution to my problem :). Thank you!

Reply

Yaaaay! Yes, that validation auto-mapping is a double-edge sword! It's nice, but when it doesn't work right, it can be really hard to debug. I'm glad we got it :).

Cheers!

Reply

Hi everyone! Great tutorial.

I've run into a weird problem.

I have created the Custom Datapersister for user.
But when i want to update a user's username, it tries to do a insert.
When i do a dump of the $user->getId() it is there.
When i do a dump of the operations name, it is a PUT.
But still it does an insert of a user.

This is my datapersister.
I've 'solved' the problem by adding a check to the support method. It checks if it is a post. If it's not, go do the original datapersistence. And it works fine.
But ofcourse I want to be able to do an update also in the UserDataPersister.

This is my code.




namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class UserDataPersister implements ContextAwareDataPersisterInterface
{
private $entityManager;
private $userPasswordEncoder;

public function __construct(
EntityManagerInterface $entityManager,
UserPasswordEncoderInterface $userPasswordEncoder
) {
$this->entityManager = $entityManager;
$this->userPasswordEncoder = $userPasswordEncoder;
}

/**
* Is the data supported by the persister?
* @param $data
* @param array $context
* @return bool
*/
public function supports($data, array $context = []): bool
{
$isPost = ($context['collection_operation_name'] ?? null) === 'post';
if (! $isPost) {
return false;
}


return $data instanceof User;
}

/**
* Persists the data.
*
* @param User $data
* @param array $context
*/
public function persist($data, array $context = [])
{
if ($data->getPlainPassword()) {
$data->setPassword(
$this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
);
$data->eraseCredentials();
}

$this->entityManager->persist($data);
$this->entityManager->flush();
}

/**
* Removes the data.
* @param $data
* @param array $context
*/
public function remove($data, array $context = []): void
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
}
Reply

Hey truuslee!

Interesting problem :). I have a few question:

A) Does this behavior *only* happen if you are changing the username? Like, if you send a PUT request that does *not* update the username (it updates some other field), does it work ok? Avoid the "roles" field - try updating some boring, non-security related field.

B) Just to verify, you workaround... *works*, right? With this workaround, you are able to send a PUT request that changes the username... and it correctly updates?

C) This should NOT make a difference, but just to be sure, if you removed the $this->entityManager->persist($data); line and tried the PUT request (which updates the username), what is the behavior? Does it work? Does it do nothing (no error, but it also doesn't update)?

As you can probably tell from my questions, I don't know the cause, but I do have some clues. Overall, when you have a situation where an entity HAS an id, but Doctrine tries to INSERT it instead of updating, the reason is that this User object has (somehow) become "detached" from Doctrine's EntityManager. *Why* that his happening in your case is the mystery.

I asked question (A) above because I think this could be related somehow to the security layer. By changing the username, your User object may suddenly look "changed" and the security system may be trying to log you out. That... does NOT explain why you are NOT getting logged out but your User object is detached, but if the username IS the only field that causes problems, then this must be related in some way. Are you using a custom user provider? Or just the normal Doctrine/entity one in security.yaml?

Phew - let me know the answers and... maybe (?) I can help :D.

Cheers!

Reply

Hi Ryan, working on sunday i see. You're crazy ! (So am i sometimes...)

My answers to your questions (Phew)

A). This behaviour also happens in another project, an update of data that has nothing todo with security. It's about saving data of an salesorder.

B). The workaround works. Everything is saved with the PUT operation.

C). That was one of the first things i tried. But then NOTHING is saved during the PUT operation.

Thank you for your great help. You are the best !

Cheers,
Annemieke

Reply

Hey truuslee!

Haha, yes, time is a bit tighter these days with a 3 year old at home 7 days a week... so work gets done whenever I can manage ;).

It sounds like you've "ruled out" my "security is the problem" idea... which didn't make complete sense anyways. But... I can't explain what's going on! What I DO know is this:

A) On a PUT (edit), API Platform queries for your object. This puts it into the "identity map" of Doctrine's UnitOfWork. Unless you have some customization, the class that should be handling that is this one: https://github.com/api-plat... - I would put some dump & die statements in that class to make sure it IS being hit (and that something else isn't handling this).

B) No, *here* is the mystery. Because of step (A), your object is in Doctrine's UnitOfWork's identity map, which means that it is aware that this object exists. Between step (A) and your data persister, something is *removing* that from the identity map. The only way I'm aware that this is possible is by something calling $entityManager->clear() or UnitOfWork::clear(). To test that theory, I would open up Doctrine's UnitOfWork.php file, find the clear() method, and throw a big giant exception at the beginning. Then try your PUT endpoint and see if it gets hit.

But what's *still* troubling me is that I can't figure out why allowing the "normal" data persister would make any difference. It - https://github.com/api-plat... - is doing the SAME thing that you're doing. This makes me think that I'm *still* missing a detail... I just can't see what it is.

By the way, if you're able to repeat this in a project that you can share, I'd be happy to take a look at it. It's very strange and you've got my interest :).

Cheers!

Reply
Romain S. Avatar
Romain S. Avatar Romain S. | posted 2 years ago

Hi symfony team !

I'm having some problems with the custom data persisting. My password is never encoded since the data persister doesn't seem to be used (when I dump inside I don't get anything back).

I've also checked the enability of autowiring and autoconfiguring but both are good.

I've didn't found any issue about this so, have you any idea of what happens ?

Thanks !

Reply

Hey Romain S.

Have you checked if support() method was called? and Does it returns right value for you persister?

Cheers!

Reply
Romain S. Avatar
Romain S. Avatar Romain S. | sadikoff | posted 2 years ago | edited

Hi sadikoff !

I've dumped inside the method and nothing comes up :/. During my test i just get a 401 cause i'm trying to loggin with a non encoded password in the database.

Reply

Weird... of course you did die() or dumped with dd() hm and what about database? have you checked values inside database user table?

Reply
Romain S. Avatar

Hey ! I finaly found that the problem come from a misktake that i hadn't seen, sorry for the inconvenience and thank you again.

Reply

I am having the same problem, how did you solve it? Thanks

Reply
Thomas L. Avatar

In my case I simpy forgot the namespace definition for the file.

Reply
Sung L. Avatar
Sung L. Avatar Sung L. | posted 2 years ago

Hi Symfony team,

I am trying to add more functions to this data persister class using ContextAwareDataPersisterInterface.
What I am going to do are:
1. when a new user is created, send email notification.
2. when an existing user information is updated, send email notification what had been changed.

I could do the first one in persist function like this:

public function persist($data, array $context = []) {
// ...
// email notification
if (
($context['collection_operation_name'] ?? null) === 'post' ||
($context['graphql_operation_name'] ?? null) === 'create'
) {
$this->sendWelcomeEmail($data);
}

$this->entityManager->persist($data);
$this->entityManager->flush();
}

However, when I tried to do the second one, to know what had been updated, I cannot find where to see the old data. For example, a user updates email address from "old_email@test.com" to "new_email@test.com", the system sends email content like this: "You updated email from old_email@test.com to new_email@test.com."

Is it possible to access old data from data persister class?

Thank you!

Reply

Hey Sung L.

There is a way to access to the previous object when working with Voters, you can check it here https://symfonycasts.com/sc...
But for what you actually want I'm not 100% sure. I think you can do it by using events https://api-platform.com/do...
or maybe you can fetch for it manually if you inject the EntityManager to your custom data persister

Cheers!

Reply
Sung L. Avatar

I thought getting data before persist will return previous data. However, when I dump $data and $previous, it is same. Here is what I have in UserDataPersister class.


public function persist($data, array $context = []) {
...
// send email notification when a new User is updated
if (($context['item_operation_name'] ?? null) === 'put') {
$previous = $this->entityManager->getRepository(User::class)->findOneBy(['id' => $data->getId()]);
$this->sendUserUpdateEmail($data, $previous);
}

$this->entityManager->persist($data);
$this->entityManager->flush();
}

Is this how you get data before update? or is it even possible to get old data in DataPersister class?

Reply

Ohh I thought the same as you but I think I know what it doesn't work. At that point, ApiPlatform has already fetched for such object and modified it, so, when you try to fetch it again, Doctrine will detect that it's already on his data set and will return to you a reference to the same object. I'm not sure if I was clear enough, but in short, you are getting the same, modified instance from Doctrine. What we need to do is to get access to the object one layer before ApiPlatform modifies it. Give it a try using events
I'll see if I can get Ryan's attention to this problem

Reply
Sung L. Avatar
Sung L. Avatar Sung L. | MolloKhan | posted 2 years ago | edited

MolloKhan The problem of using the Event System is GraphQL is not supported. I wonder how I can achieve the goal with both REST and GraphQL supports.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}