Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Lucky you! You found an early release chapter - it will be fully polished and published shortly!

Reusable Entity->Dto Provider & Proccesor

This Chapter isn't
quite ready...

Rest assured, the gnomes are hard at work
completing this video!

Browse Tutorials

Our UserAPI is now a fully functional API resource class! We've got our EntityToDtoStateProvider, which calls the core state provider from Doctrine, and that gives us all the good stuff, like querying, filtering, and pagination. Then, down here, we leverage the MicroMapper system to convert the $entity objects into UserApi objects.

And we do the same thing with the processor. We use MicroMapper to go from UserApi to our User entity... then call the core Doctrine state processor to let it do the saving or deleting. I love that!

At this point, our dream is to create a DragonTreasureApi and repeat all of this magic. And if we can make these processor and provider classes completely generic... that's going to be super easy. So let's do it!

Making the Provider Generic

Start in the provider. If you search for "user", there's only one spot: where we tell MicroMapper which class to convert our $entity into. So... can we fetch this dynamically? Up here, our provider receives the $operation along with $context. Let's dump both of these.

And since this is in our provider... we can just go refresh the Collection endpoint and... boom! This is a GetCollection operation... check it out. The operation object stores the ApiResource class that it's attached to!

So over here, it's simple: $resourceClass = $operation->getClass(). Now that we've got that, down here, make it an argument - string $resourceClass - and pass that instead. Finally, we need to add $resourceClass as the argument when we call mapEntityToDto() there... and right there. Remove the use statement we don't need anymore and... just like that, it still work!

Making the Processor Generic

We're on a roll! Head to the processor and search for "user". Ah, we have the same problem except, this time, we need the User entity class.

Ok! Up on top, dd($operation). And for this, we'll need to run one of our tests:

symfony php bin/phpunit --filter=testPostToCreateUser

And... got it! We see the Post operation.. and the class is, of course, UserApi. But, in this case, what we really need is the User class. Remember: in UserApi, we used stateOptions to say that UserApi is tied to the User entity. And now, we can read this info from the operation. If we scroll down a bit... there it is: the stateOptions property with the Options object, and entityClass inside.

Cool! Back in the processor, towards the top... remove the dd() and start with $stateOptions = $operation->getStateOptions(). Then, to help my editor (and also in case of any misconfiguration), assert($stateOptions instanceof Options) (the one from Doctrine ORM).

You can technically use different Options classes for your $stateOptions... like if you're getting data from ElasticSearch, but we we know we're using this one from Doctrine. Below that, say $entityClass = $stateOptions->getEntityClass().

And... we don't need this assert() down here, thenpass $entityClass to mapDtoToEntity(). Finally, use that with string $entityClass... and also pass it here.

When we search for "user" now... we can get rid of the two use statements... and... we're clean! It's generic! Try the test!

symfony php bin/phpunit --filter=testPostToCreateUser

And... that's it! We're ready! We have a reusable provider and processor! Next, let's create a DragonTreasureApi class, repeat this magic, and see how quickly things fall into place.

Leave a comment!

Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "3.1.x-dev", // 3.1.x-dev
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.10.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
        "doctrine/orm": "^2.14", // 2.16.1
        "nelmio/cors-bundle": "^2.2", // 2.3.1
        "nesbot/carbon": "^2.64", // 2.69.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.23.1
        "symfony/asset": "6.3.*", // v6.3.0
        "symfony/console": "6.3.*", // v6.3.2
        "symfony/dotenv": "6.3.*", // v6.3.0
        "symfony/expression-language": "6.3.*", // v6.3.0
        "symfony/flex": "^2", // v2.3.3
        "symfony/framework-bundle": "6.3.*", // v6.3.2
        "symfony/property-access": "6.3.*", // v6.3.2
        "symfony/property-info": "6.3.*", // v6.3.0
        "symfony/runtime": "6.3.*", // v6.3.2
        "symfony/security-bundle": "6.3.*", // v6.3.3
        "symfony/serializer": "6.3.*", // v6.3.3
        "symfony/stimulus-bundle": "^2.9", // v2.10.0
        "symfony/string": "6.3.*", // v6.3.2
        "symfony/twig-bundle": "6.3.*", // v6.3.0
        "symfony/ux-react": "^2.6", // v2.10.0
        "symfony/ux-vue": "^2.7", // v2.10.0
        "symfony/validator": "6.3.*", // v6.3.2
        "symfony/webpack-encore-bundle": "^2.0", // v2.0.1
        "symfony/yaml": "6.3.*", // v6.3.3
        "symfonycasts/micro-mapper": "^0.1.0" // v0.1.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.11
        "symfony/browser-kit": "6.3.*", // v6.3.2
        "symfony/css-selector": "6.3.*", // v6.3.2
        "symfony/debug-bundle": "6.3.*", // v6.3.2
        "symfony/maker-bundle": "^1.48", // v1.50.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.3.2
        "symfony/stopwatch": "6.3.*", // v6.3.0
        "symfony/web-profiler-bundle": "6.3.*", // v6.3.2
        "zenstruck/browser": "^1.2", // v1.4.0
        "zenstruck/foundry": "^1.26" // v1.35.0