Buy Access to Course
32.

Triggering a "Publish"

|

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're down to just one test failure: it's in testPublishTreasure. Let's check it out. Ok, this tests to make sure that a notification is created in the database when the status of a treasure changes from 'isPublished' => false to 'isPublished' => true. Previously, we implemented this via a custom state processor.

But now, we could put this into our mapper class! In DragonTreasureApiToEntityMapper, we could check to see if the entity was 'isPublished' => false and is now changing to 'isPublished' => true. If it is, create a notification right there. If this sounds good to you, go for it!

However, for me, putting the logic here doesn't quite feel right... just because it's a "data mapper", so it smells a bit strange to do something beyond just mapping the data.

Creating the State Processor

So, let's go back to our original solution: creating a state processor. Over at you terminal, run:

php bin/console make:state-processor

Call it DragonTreasureStateProcessor. Our goal should feel familiar: we'll add some custom logic here, but call the normal state processor to let it do the heavy lifting.

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

To do that, add a __construct() method with private EntityClassDtoStateProcessor $innerProcessor. Down here, use that with return $this->innerProcessor->process() passing the arguments it needs: $data, $operation, $uriVariables, and $context. Ah, and you can see this is highlighted in red. This isn't really a void method, so remove that.

21 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 7
class DragonTreasureStateProcessor implements ProcessorInterface
{
public function __construct(
private EntityClassDtoStateProcessor $innerProcessor,
)
{
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
return $this->innerProcessor->process($data, $operation, $uriVariables, $context);
}
}

Ok, let's hook up our API resource to use this! Inside DragonTreasureApi, change the processor to DragonTreasureStateProcessor.

75 lines | src/ApiResource/DragonTreasureApi.php
// ... lines 1 - 13
use App\State\DragonTreasureStateProcessor;
// ... lines 15 - 20
#[ApiResource(
// ... lines 22 - 37
processor: DragonTreasureStateProcessor::class,
// ... line 39
)]
class DragonTreasureApi
{
// ... lines 43 - 73
}

At this point, we haven't really changed anything: the system will call our new processor... but then it just calls the old one. And so when we run the tests:

symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php

Everything still works except for that last failure.

Detecting the isPublished Change

So let's add our notification code! Originally, we figured out if isPublished was changing from false to true by using the "previous data" that's inside the $context. Dump $context['previous_data'] to see what that looks like.

23 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 7
class DragonTreasureStateProcessor implements ProcessorInterface
{
// ... lines 10 - 15
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
dd($context['previous_data']);
// ... lines 19 - 20
}
}

Now, run just this test:

symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php --filter=testPublishTreasure

Cool! The previous data is the DragonTreasureApi with isPublished: false.. because that's the value our entity starts with in the test. Let's also dump $data.

symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php --filter=testPublishTreasure

Okay, the original one has isPublished: false, and the new one has isPublished: true! And that makes us dangerous.

Back over, we wrote the notification code in a previous tutorial... so I'll just paste it in. This is delightfully boring! We use $previousData and $data to detect the state change from isPublished false to true... then create a Notification.

44 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 12
class DragonTreasureStateProcessor implements ProcessorInterface
{
// ... lines 15 - 22
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
assert($data instanceof DragonTreasureApi);
$result = $this->innerProcessor->process($data, $operation, $uriVariables, $context);
$previousData = $context['previous_data'] ?? null;
if ($previousData instanceof DragonTreasureApi
&& $data->isPublished
&& $previousData->isPublished !== $data->isPublished
) {
$entity = $this->repository->find($data->id);
$notification = new Notification();
$notification->setDragonTreasure($entity);
$notification->setMessage('Treasure has been published!');
$this->entityManager->persist($notification);
$this->entityManager->flush();
}
return $result;
}
}

The only thing that's kind of interesting is that the Notification entity is related to a DragonTreasure entity... so we query for the $entity using the repository and the id from the DTO class.

Let's inject the services we need: private EntityManagerInterface $entityManager so we can save and private DragonTreasureRepository $repository.

44 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 8
use App\Entity\Notification;
use App\Repository\DragonTreasureRepository;
use Doctrine\ORM\EntityManagerInterface;
class DragonTreasureStateProcessor implements ProcessorInterface
{
public function __construct(
private EntityClassDtoStateProcessor $innerProcessor,
private EntityManagerInterface $entityManager,
private DragonTreasureRepository $repository,
)
{
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
assert($data instanceof DragonTreasureApi);
$result = $this->innerProcessor->process($data, $operation, $uriVariables, $context);
$previousData = $context['previous_data'] ?? null;
if ($previousData instanceof DragonTreasureApi
&& $data->isPublished
&& $previousData->isPublished !== $data->isPublished
) {
$entity = $this->repository->find($data->id);
$notification = new Notification();
$notification->setDragonTreasure($entity);
$notification->setMessage('Treasure has been published!');
$this->entityManager->persist($notification);
$this->entityManager->flush();
}
return $result;
}
}

There we go! Moment of truth:

symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php --filter=testPublishTreasure

The test passes! Heck, at this point, all of our treasure tests pass! We've completely converted this complex API resource to our DTO-powered system! High five!

Next: Let's make it possible to write the $owner property on dragon treasure. This involves a trick that will help us better understand how API Platform loads relation data.