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 SubscribeTo get an output DTO class to... ya know... actually work, we need to write code that converts our CheeseListing
object into a CheeseListingOutput
object so that API Platform can serialize it. The "thing" that does that is called a data transformer.
Let's create one in the src/
directory: add a new directory called DataTransformer
for organization and a class inside called CheeseListingOutputDataTransformer
:
... lines 1 - 2 | |
namespace App\DataTransformer; | |
... lines 4 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
... lines 11 - 19 | |
} |
As usual with a class that hooks into part of API Platform, this needs to implement an interface. In this situation, it's - surprise! - DataTransformerInterface
!
... lines 1 - 4 | |
use ApiPlatform\Core\DataTransformer\DataTransformerInterface; | |
... lines 6 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
... lines 11 - 19 | |
} |
Inside the class, go to "Code"->"Generate" - or Command
+N
on a Mac - and select "Implement Methods" to generate the two we need:
... lines 1 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
public function transform($object, string $to, array $context = []) | |
{ | |
} | |
public function supportsTransformation($data, string $to, array $context = []): bool | |
{ | |
} | |
} |
So here's how this works: the data transformer system exists entirely to support the input and output DTO classes that we're working with. Whenever API Platform is about to serialize an object, it checks to see if that resource has an output DTO - which we configured in CheeseListing's @ApiResource
annotation. If it does, it loops over every data transformer in the system and calls supportsTransformation()
.
It basically asks each one:
Hey! Apparently I need to transform a
CheeseListing
into aCheeseListingOutput
. Do... you know how to do that?
And thanks to auto-configuration, because our new class implements DataTransformerInterface
, it's instantly part of that system. In other words, our supportsTransformation()
method should now be called!
To prove it is, lets dd($data)
and $to
:
... lines 1 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
... lines 11 - 15 | |
public function supportsTransformation($data, string $to, array $context = []): bool | |
{ | |
dd($data, $to); | |
} | |
} |
Now, move over and refresh the endpoint. There it is! API Platform is passing us the CheeseListing
and for the $to
argument, it's asking:
Do you know how to convert this
CheeseListing
intoCheeseListingOutput
?
And we do! For supportsTransformation()
, return $data instanceof CheeseListing
and $to === CheeseListingOutput::class
:
... lines 1 - 5 | |
use App\Dto\CheeseListingOutput; | |
use App\Entity\CheeseListing; | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
... lines 11 - 15 | |
public function supportsTransformation($data, string $to, array $context = []): bool | |
{ | |
return $data instanceof CheeseListing && $to === CheeseListingOutput::class; | |
} | |
} |
That second part might seem unnecessary... since, in our app, a CheeseListing
will always have CheeseListingOutput
as its output class. But technically, you can configure a different output
class on an operation-by-operation basis. So, we're checking it to be safe.
As soon as one of the data transformers returns true
from supportsTransformation()
, API Platform will call transform()
so that we can do our work. To make sure that's happening, dd($object)
and $to
:
... lines 1 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
public function transform($object, string $to, array $context = []) | |
{ | |
dd($object, $to); | |
} | |
... lines 15 - 19 | |
} |
When we move over and refresh... yes! It dumps the exact same thing.
Back in transform()
, we know that $object
will be a CheeseListing
object. Let's rename $object
to $cheeseListing
and then, above this, add PHPDoc to tell my editor that this will be a CheeseListing
object:
... lines 1 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
/** | |
* @param CheeseListing $cheeseListing | |
*/ | |
public function transform($cheeseListing, string $to, array $context = []) | |
{ | |
... lines 16 - 19 | |
} | |
... lines 21 - 25 | |
} |
Ok: our job in transform()
is pretty simple: return a CheeseListingOutput
object. Let's do this as simply as we can: $output = new CheeseListingOutput()
. And then, the only field we have right now is title
. Populate that with $output->title = $cheeseListing->getTitle()
. At the bottom, return $output
:
... lines 1 - 8 | |
class CheeseListingOutputDataTransformer implements DataTransformerInterface | |
{ | |
... lines 11 - 13 | |
public function transform($cheeseListing, string $to, array $context = []) | |
{ | |
$output = new CheeseListingOutput(); | |
$output->title = $cheeseListing->getTitle(); | |
return $output; | |
} | |
... lines 21 - 25 | |
} |
Let's do this! Move back over, refresh and... um... it kind of works? We are getting results... but each one only has @id
?
I have two questions about this. First... what happened to @type
? Each item usually has at least @id
and @type
. So where is @type
hiding? We'll talk about why that's missing a bit later.
But before that, my second question is: why don't we see the title
field? That has a simpler answer: normalization groups.
On CheeseListing
, we set a normalizationContext
with groups
set to cheese:read
:
... lines 1 - 20 | |
/** | |
* @ApiResource( | |
... line 23 | |
* normalizationContext={"groups"={"cheese:read"}}, | |
... lines 25 - 47 | |
* ) | |
... lines 49 - 61 | |
*/ | |
class CheeseListing | |
{ | |
... lines 65 - 221 | |
} |
Thanks to the output DTO, what's actually being serialized now is a CheeseListingOutput
object. But, the normalization groups still apply.
In other words, in CheeseListingOutput
, we need to add that group above any properties that we want to serialize. Above title
, say @Groups()
, go copy the group name, and paste it here: cheese:read
:
... lines 1 - 4 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
class CheeseListingOutput | |
{ | |
/** | |
* @Groups({"cheese:read"}) | |
*/ | |
public $title; | |
} |
Now when we try it... sweet! We have a title
field!
Next: let's add the rest of the properties we need to CheeseListingOutput
and see how all of this looks in our documentation. Because... similar to DailyStats
, since this is not an entity, we're going to need to do a bit more work to help API Platform understand the types of each property.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.5.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.1.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
"doctrine/orm": "^2.4.5", // 2.8.2
"nelmio/cors-bundle": "^2.1", // 2.1.0
"nesbot/carbon": "^2.17", // 2.39.1
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
"ramsey/uuid-doctrine": "^1.6", // 1.6.0
"symfony/asset": "5.1.*", // v5.1.5
"symfony/console": "5.1.*", // v5.1.5
"symfony/debug-bundle": "5.1.*", // v5.1.5
"symfony/dotenv": "5.1.*", // v5.1.5
"symfony/expression-language": "5.1.*", // v5.1.5
"symfony/flex": "^1.1", // v1.18.7
"symfony/framework-bundle": "5.1.*", // v5.1.5
"symfony/http-client": "5.1.*", // v5.1.5
"symfony/monolog-bundle": "^3.4", // v3.5.0
"symfony/security-bundle": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/validator": "5.1.*", // v5.1.5
"symfony/webpack-encore-bundle": "^1.6", // v1.8.0
"symfony/yaml": "5.1.*" // v5.1.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
"symfony/browser-kit": "5.1.*", // v5.1.5
"symfony/css-selector": "5.1.*", // v5.1.5
"symfony/maker-bundle": "^1.11", // v1.23.0
"symfony/phpunit-bridge": "5.1.*", // v5.1.5
"symfony/stopwatch": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/web-profiler-bundle": "5.1.*", // v5.1.5
"zenstruck/foundry": "^1.1" // v1.8.0
}
}
Hi,
Thanks for your tutorials, they rock !!!
I'm having trouble using dtos with validation groups. Validation works but the validation_groups set in the Entity class attribute is not interpreted on the dataTransformer call.
My Entity definition :