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.
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!
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.
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
:
... 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
:
Tip
Instead of this long string, API Platform also creates an "alias service" to the core processor's class name. This allows you to use:
use ApiPlatform\Doctrine\Common\State\PersistProcessor;
// ...
#[AsDecorator(PersistProcessor::class)]
... 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!
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.
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.
Hey @Sebastian-K!
Hmmm. How have you set things up so that the "registration" endpoint has a firstName
field but without User
having a firstName
property? Usually I WOULD have this as a property on User
, or I might make a DTO for this specific operation if you've got things split up.
But anyway, this is an interesting problem! The JSON is ready from the request and passed directly to the serializer here: https://github.com/api-platform/core/blob/main/src/Symfony/EventListener/DeserializeListener.php#L98-L101
The problem is that, if your "resource class" for this operation is User
and it doesn't have a firstName
property, then that field from the JSON is simply ignored. I think the only way to get the firstName
field would be to grab the $request->getContent()
and json_decode()
it manually. But... I really hope we can find a better way :).
Cheers!
Hi team, my question is: if I use Symfony with Api Platform and Easy Admin, where I have to write "unified" logic to hash the password for both applications?
I think that in that case a listener/subscriber is better, right?
Hey @Fedale!
That's a great question. I can think of 2 options, and they're both totally fine imo:
persistEntity()
in your controller. Duplication sounds lame... but password hashing logic is already SO simple (it's just 1 line basically) that you are not really duplicating much.Cheers!
Does it really make sense to set up decoration for the UserHashPasswordProcessor via #[AsDecorator()]? As I understand it the decorating service is then involved in every call of the PersistProcessor?
In the API Platform docs they use a "bind" in the services to bind the $persistProcessor as an argument to the "UserPasswordHasher".
This way I guess it is only decorating the service when it is used (e.g. defining the "processor" on operation level)...
I'm building this tutorial not in the project but in a custom bundle.
Since it took me some time to find the solution for my case, I'd like to post it here for others that might struggle with that.
I did NOT set the #[AsDecorator(PersistProcessor::class)]
attribute in UserHashPasswordProcessor
In the User Entity I added processor: UserHashPasswordProcessor::class
to Put, Post and Patch.
Example:
new Put(
security: 'is_granted("ROLE_USER_EDIT")',
processor: UserHashPasswordProcessor::class
)
In the bundles services.xml I added:
<service id="Acme\MyBundle\State\UserHashPasswordProcessor" autowire="true" autoconfigure="true">
<bind key="$processor" id="api_platform.doctrine.orm.state.persist_processor" type="service"/>
</service>
Since this Processor is called from the User
Entity now, I also modified the process
method in UserHashPasswordProcessor
a litte:
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
if ($data->getPlainPassword()) {
$hashedPassword = $this->userPasswordHasher->hashPassword($data, $data->getPlainPassword());
$data->setPassword($hashedPassword);
$data->eraseCredentials();
}
$this->processor->process($data, $operation, $uriVariables, $context);
}
If this isn't smart, please correct me :-)
Hey @Tobias-B!
Your thinking on this is absolutely correct. For me, it was a trade-off between complexity (the API Platform official way is more complex) vs potential performance problems. So, the final decision is subjective, but since PersistProcessor
is only called during POST/PUT/PATCH operations and it will only be called once (I would be more concerned if PersistProessor
were called many times during a single request) and the logic inside of UserHashPasswordProcessor
is really simple in those cases (if not User
, it exits immediately), I think the performance issue is non-material. So, I went for simplicity :). But I think the other approach is 110% valid - so you can choose your favorite.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.1.2
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.8.3
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.1
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.66.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.16.1
"symfony/asset": "6.2.*", // v6.2.5
"symfony/console": "6.2.*", // v6.2.5
"symfony/dotenv": "6.2.*", // v6.2.5
"symfony/expression-language": "6.2.*", // v6.2.5
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.5
"symfony/property-access": "6.2.*", // v6.2.5
"symfony/property-info": "6.2.*", // v6.2.5
"symfony/runtime": "6.2.*", // v6.2.5
"symfony/security-bundle": "6.2.*", // v6.2.6
"symfony/serializer": "6.2.*", // v6.2.5
"symfony/twig-bundle": "6.2.*", // v6.2.5
"symfony/ux-react": "^2.6", // v2.7.1
"symfony/ux-vue": "^2.7", // v2.7.1
"symfony/validator": "6.2.*", // v6.2.5
"symfony/webpack-encore-bundle": "^1.16", // v1.16.1
"symfony/yaml": "6.2.*" // v6.2.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.3
"symfony/browser-kit": "6.2.*", // v6.2.5
"symfony/css-selector": "6.2.*", // v6.2.5
"symfony/debug-bundle": "6.2.*", // v6.2.5
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.2.5
"symfony/stopwatch": "6.2.*", // v6.2.5
"symfony/web-profiler-bundle": "6.2.*", // v6.2.5
"zenstruck/browser": "^1.2", // v1.2.0
"zenstruck/foundry": "^1.26" // v1.28.0
}
}
Is there a way to process additional fields from the request?
For example, for the registration, I send,
email
,password
andfirstname
.email
andpassword
goes to theUser
entity and thefirstname
in theProfile
entity (that's created in aStateProcessor
, but there, I don't have access to the original POST request/data