State Processors: Hashing the User 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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen an API client creates a user, they send a password
field, which gets set onto the plainPassword
property. Now, we need to hash that password before the User
is saved to the database. Like we showed when working with Foundry, hashing a password is simple: grab the UserPasswordHasherInterface
service then call a method on it:
// ... lines 1 - 6 | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
// ... lines 8 - 30 | |
final class UserFactory extends ModelFactory | |
{ | |
// ... lines 33 - 47 | |
public function __construct( | |
private UserPasswordHasherInterface $passwordHasher | |
) | |
{ | |
// ... line 52 | |
} | |
// ... lines 54 - 81 | |
protected function initialize(): self | |
{ | |
return $this | |
->afterInstantiate(function(User $user): void { | |
$user->setPassword($this->passwordHasher->hashPassword( | |
$user, | |
$user->getPassword() | |
)); | |
}) | |
; | |
} | |
// ... lines 93 - 97 | |
} |
But to pull this off, we need a "hook" in API platform: we need some way to run code after our data is deserialized onto the User
object, but before it's saved.
In our tutorial about API platform 2, we used a Doctrine listener for this, which would still work. Though, it does some negatives, like being super magical - it's hard to debug if it doesn't work - and you need to do some weird stuff to make sure it runs when editing a user's password.
Hello State Processors
Fortunately, In API platform 3, we have a shiny new tool that we can leverage. It's called a state processor. And actually, our User
class is already using a state processor!
Find the API Platform 2 to 3 upgrade guide... and search for processor. Let's see... here we go. It has a section called providers and processors. We'll talk about providers later.
According to this, if you have an ApiResource
class that is an entity - like in our app - then, for example, your Put
operation already uses a state processor called PersistProcessor
! The Post
operation also uses that, and Delete
has one called RemoveProcessor
.
State processors are cool. After the sent data is deserialized onto the object, we... need to do something! Most of the time, that "something" is: save the object to the database. And that's precisely what PersistProcessor
does! Yea, our entity changes are saved to the database entirely thanks to that built-in state processor!
Creating the Custom State Processor
So here's the plan: we're going to hook into the state processor system and add our own. Step one, run a new command from API Platform:
php ./bin/console make:state-processor
Let's call it UserHashPasswordProcessor
. Perfect.
Spin over, go into src/
, open the new State/
directory and check out UserHashPasswordStateProcessor
:
// ... lines 1 - 2 | |
namespace App\State; | |
use ApiPlatform\Metadata\Operation; | |
use ApiPlatform\State\ProcessorInterface; | |
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
// Handle the state | |
} | |
} |
It's delightfully simple: API platform will call this method, pass us data, tell us which operation is happening... and a few other things. Then... we just do whatever we want. Send emails, save things to the database, or RickRoll someone watching a screencast!
Activating this processor is simple in theory. We could go to the Post
operation, add a processor
option and set it to our service id: UserHashPasswordStateProcessor::class
.
Unfortunately... if we did that, it would replace the PersistProcessor
that it's using now. And... we don't want that: we want our new processor to run... and then also the existing PersistProcessor
. But... each operation can only have one processor.
Setting up Decoration
No worries! We can do this by decorating PersistProcessor
. Decoration always follows the same pattern. First, add a constructor that accept an argument with the same interface as our class: private ProcessorInterface
and I'll call it $innerProcessor
:
// ... lines 1 - 5 | |
use ApiPlatform\State\ProcessorInterface; | |
// ... lines 7 - 9 | |
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
public function __construct(private ProcessorInterface $innerProcessor) | |
{ | |
} | |
// ... lines 15 - 21 | |
} |
After I add a dump()
to see if this is working, we'll do step 2: call the decorated service method: $this->innerProcessor->process()
passing $data
, $operation
, $uriVariables
and... yes, $context
:
Tip
In API Platform 3.2 and higher, you should return $this->innerProcessor->process()
. This
is also a safe thing to do in 3.0 & 3.1.
// ... lines 1 - 9 | |
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 12 - 15 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
dump('ALIVE!'); | |
$this->innerProcessor->process($data, $operation, $uriVariables, $context); | |
} | |
} |
Love it: our class is set up for decoration. Now we need to tell Symfony to use it. Internally, PersistProcessor
from API Platform is a service. We're going to tell Symfony that whenever anything needs that PersistProcessor
service, it should be passed our service instead... but also that Symfony should pass us the original PersistProcessor
.
To do that, add #[AsDecorator()]
and pass the id of the service. You can usually find this in the documentation, or you can use the debug:container
command to search for it. The docs say it's api_platform.doctrine.orm.state.persist_processor
:
// ... lines 1 - 6 | |
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
'api_platform.doctrine.orm.state.persist_processor') | (|
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 12 - 21 | |
} |
Decoration done! We're not doing anything yet, but let's see if it hits our dump! Run the test:
symfony php bin/phpunit tests/Functional/UserResourceTest.php
And... there it is! It's still a 500, but it is using our processor!
Adding the Hashing Logic
Now we can get to work. Because of how we did the service decoration, our new processor will be called whenever any entity is processed... whether it's a User
, DragonTreasure
or something else. So, start by checking if $data
is an instanceof User
... and if $data->getPlainPassword()
... because if we're editing a user, and no password
is sent, no need for us to do anything:
// ... lines 1 - 11 | |
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 14 - 17 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
if ($data instanceof User && $data->getPlainPassword()) { | |
// ... line 21 | |
} | |
$this->innerProcessor->process($data, $operation, $uriVariables, $context); | |
} | |
} |
By the way, the official documentation for decorating state processors is slightly different. It looks more complex to me, but the end result is a processor that's only called for one entity, not all of them.
To hash the password, add a second argument to the constructor: private UserPasswordHasherInterface
called $userPasswordHasher
:
// ... lines 1 - 8 | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
// ... lines 10 - 11 | |
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
public function __construct(private ProcessorInterface $innerProcessor, private UserPasswordHasherInterface $userPasswordHasher) | |
{ | |
} | |
// ... lines 17 - 25 | |
} |
Below, say $data->setPassword()
set to $this->userPasswordHasher->hashPassword()
passing it the User
, which is $data
and the plain password: $data->getPlainPassword()
:
// ... lines 1 - 11 | |
class UserHashPasswordStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 14 - 17 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
if ($data instanceof User && $data->getPlainPassword()) { | |
$data->setPassword($this->userPasswordHasher->hashPassword($data, $data->getPlainPassword())); | |
} | |
$this->innerProcessor->process($data, $operation, $uriVariables, $context); | |
} | |
} |
And this all happens before we call the inner processor that actually saves the object.
Let's try this thing! Run that test:
symfony php bin/phpunit tests/Functional/UserResourceTest.php
Victory! After creating a user in our API, we can then log in as that user.
User.eraseCredentials()
Oh, and it's minor, but once you have a plainPassword
property, inside of User
, there's a method called eraseCredentials()
. Uncomment $this->plainPassword = null
:
// ... lines 1 - 67 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
// ... lines 70 - 186 | |
public function eraseCredentials() | |
{ | |
// If you store any temporary, sensitive data on the user, clear it here | |
$this->plainPassword = null; | |
} | |
// ... lines 192 - 292 | |
} |
This makes sure that if the object is serialized into the session, the sensitive plainPassword
is cleared first.
Next: let's fix some validation issues via validationGroups
and discover something special about the Patch
operation.
Hi ! when I use the process method I need to return the result of the process method of the decorated class. else my POST call return an empty string.