Buy Access to Course
20.

DTO -> Entity State Processor

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

We've checked off the "provider" side of things for our new UserApi class. So let's shift our focus to the processor so we can save things. And we do have some rather delightful tests for our User endpoints. Open UserResourceTest.

The Anatomy of the Request & State Processor

Ok, testPostToCreateUser(), posts some data, creates the user, then tests to make sure that the password we posted works by logging in. Add ->dump() to help us see what's going on. Then, copy that method name and run it:

symfony php bin/phpunit --filter=testPostToCreateUser

No surprise... it fails:

Current response status code is 400, but 201 expected.

The dump is really helpful. It's our favorite error!

Unable to generate an IRI for the item of type UserApi.

We already talked about what's happening: the JSON is deserialized into a UserApi object. Good! Then the core Doctrine PersistProcessor is called because that's the default processor when using stateOptions. But... because our UserApi isn't an entity, PersistProcessor does nothing. Finally, API Platform serializes the UserApi back into JSON... but without the id populated, it fails to generate the IRI.

Watch! Over in UserApi, temporarily default $id to 5. When we try the test now...

symfony php bin/phpunit --filter=testPostToCreateUser

It appears to work. Ok, it fails... but only later... down here in UserResourceTest line 33. It is getting through the POST successfully.

Creating the State Processor

Look at the response on top, it is returning this user JSON. But, still, nothing is saving. Change the id back to null. We need to fix this lack of saving by creating a new state processor. So spin over and run:

php bin/console make:state-processor

Call it EntityClassDtoStateProcessor because, again, we're going to make this class generic so that it works for any API resource class that's tied to a Doctrine entity. We'll use it later for DragonTreasure.

15 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 2
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
// Handle the state
}
}

With the empty processor generated, go hook it up in UserApi with processor: EntityClassDtoStateProcessor::class.

39 lines | src/ApiResource/UserApi.php
// ... lines 1 - 9
use App\State\EntityClassDtoStateProcessor;
// ... lines 11 - 13
#[ApiResource(
// ... lines 15 - 17
processor: EntityClassDtoStateProcessor::class,
// ... line 19
)]
// ... lines 21 - 23
class UserApi
{
// ... lines 26 - 37
}

Henceforth, every time we POST, PATCH, or DELETE this resource, this processor will be called.

Mapping the DTO Back to an Entity

But what is this $data variable exactly? You may have a guess, but just in case, let's dd($data)... and rerun the test.

39 lines | src/ApiResource/UserApi.php
// ... lines 1 - 9
use App\State\EntityClassDtoStateProcessor;
// ... lines 11 - 13
#[ApiResource(
// ... lines 15 - 17
processor: EntityClassDtoStateProcessor::class,
// ... line 19
)]
// ... lines 21 - 23
class UserApi
{
// ... lines 26 - 37
}
symfony php bin/phpunit --filter=testPostToCreateUser

Yup, it's a UserApi object! The JSON we sent is deserialized into this UserApi object, and then that is passed to our state processor. The UserApi object is the "central object" inside of API Platform for this request.

Our job in the state processor is simple but important: to convert this UserApi back to a User entity so that we can save it. Say assert($data instanceof UserApi) and, inside, $entity = set to a new helper function: $this->mapDtoToEntity($data). Below, dd($entity).

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 6
use App\ApiResource\UserApi;
// ... lines 8 - 10
class EntityClassDtoStateProcessor implements ProcessorInterface
{
// ... lines 13 - 19
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
assert($data instanceof UserApi);
$entity = $this->mapDtoToEntity($data);
dd($entity);
}
// ... lines 27 - 47
}

Then go add that new private function mapDtoToEntity(), which will accept an object $dto argument and return another object.

Again, we know this will really accept a UserApi object and return a User entity... but we're trying to keep this class generic so we can reuse it later. Though we are going to have some user-specific code down here temporarily. In fact, to help our editor, add another assert($dto instanceof UserApi).

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 6
use App\ApiResource\UserApi;
// ... lines 8 - 10
class EntityClassDtoStateProcessor implements ProcessorInterface
{
// ... lines 13 - 19
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
assert($data instanceof UserApi);
$entity = $this->mapDtoToEntity($data);
dd($entity);
}
private function mapDtoToEntity(object $dto): object
{
assert($dto instanceof UserApi);
// ... lines 31 - 46
}
}

Querying for the Existing Entity

We need to think about two different cases. The first is when we POST to create a brand-new user. In that case, $dto will have a null id. And that means we should create a fresh User object. The other case is if we were making, for example, a PATCH request to edit a user. In that case, the item provider will first load that User entity from the database... our provider will turn that into a UserApi object with id equal to 6... and that will eventually be passed to us here. If the id is 6... we don't want to create a new User object: we want to query the database for tha existing User. Our job is to handle both situations.

Undo the changes to the test so we don't break anything... and now, if $dto->id, we need to query for an existing User. To do that, on top, add a constructor with private UserRepository $userRepository.

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 8
use App\Repository\UserRepository;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function __construct(
private UserRepository $userRepository
)
{
}
// ... lines 19 - 47
}

Back down here, say $entity = $this->userRepository->find($dto->id).

If we don't find that User, throw a big giant exception that will trigger a 500 error with Entity %d not found.

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 8
use App\Repository\UserRepository;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function __construct(
private UserRepository $userRepository
)
{
}
// ... lines 19 - 27
private function mapDtoToEntity(object $dto): object
{
assert($dto instanceof UserApi);
if ($dto->id) {
$entity = $this->userRepository->find($dto->id);
if (!$entity) {
throw new \Exception(sprintf('Entity %d not found', $dto->id));
}
// ... lines 37 - 38
}
// ... lines 40 - 46
}
}

You might be wondering:

Shouldn't this trigger a 404 error instead?

The answer, in this case, is no. If we're in this situation, it means the item state provider has already successfully queried for a User with this id. So there should be no way for us to suddenly not find it. There are some exceptions to this, like if you allowed your user to change their id... or if you allowed users to create brand-new objects and set the id manually... but for most situations, including ours, if this happens, something went weird.

Next up, if we don't have an id, say $entity = new User().

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 7
use App\Entity\User;
use App\Repository\UserRepository;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function __construct(
private UserRepository $userRepository
)
{
}
// ... lines 19 - 27
private function mapDtoToEntity(object $dto): object
{
assert($dto instanceof UserApi);
if ($dto->id) {
$entity = $this->userRepository->find($dto->id);
if (!$entity) {
throw new \Exception(sprintf('Entity %d not found', $dto->id));
}
} else {
$entity = new User();
}
// ... lines 40 - 46
}
}

Done! In both cases, down here, we're going to map the $dto object to the $entity object. This code is boring... so I'll speed through this. For the password, put a TODO temporarily because we still need to hash that. Also add a TODO for handle dragon treasures. Just focus on the easy stuff... and at the bottom, return $entity.

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 7
use App\Entity\User;
use App\Repository\UserRepository;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function __construct(
private UserRepository $userRepository
)
{
}
// ... lines 19 - 27
private function mapDtoToEntity(object $dto): object
{
assert($dto instanceof UserApi);
if ($dto->id) {
$entity = $this->userRepository->find($dto->id);
if (!$entity) {
throw new \Exception(sprintf('Entity %d not found', $dto->id));
}
} else {
$entity = new User();
}
$entity->setEmail($dto->email);
$entity->setUsername($dto->username);
$entity->setPassword('TODO properly');
// TODO: handle dragon treasures
return $entity;
}
}

If we've done things correctly, we'll take the UserApi, transform that into an $entity and dump it. Rerun the test:

symfony php bin/phpunit --filter=testPostToCreateUser

And... 404! Let's see what happened here. Oh... of course. I never put my test back together. This should be ->post('/api/users'). Try that again and... got it! There's our User entity object with the email and username transferred correctly!

Next: Let's save this by leveraging the core Doctrine PersistProcessor and RemoveProcessor. We'll also handle hashing the password. By the end, our user tests will be passing with flying colors.