> APIs >

Course Overview

API Platform 3 Part 3: Custom Resources

  • 836 students
  • EN/ES Captions
  • EN/ES Script
  • Certificate of Completion

Your Guides

About this course

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
    }
}

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
    }
}

Thanks to part 1 & part 2, we've already built a seriously powerful API, complete with security, custom fields and many more goodies. In this course, we'll take things even further:

  • State Providers & "proper" custom fields
  • Run custom code on a "state" change (e.g. publishing)
  • Custom (non-entity) DTO #[ApiResource] classes
  • The new stateOptions shortcut for DTO's
  • DTO's & state providers (make:state-provider)
  • State processors with DTO's
  • Data transformation with symfonycasts/micro-mapper
  • IGNORED_ATTRIBUTES, security & other tricks to avoid serialization groups
  • Embedded objects (including non-ApiResource objects)
  • Pagination for DTO resources

Woh. If you thought you were dangerous before with API Platform, just wait...

Next courses in the APIs: API Platform 3 section of the APIs Track!

14 Comments

Sort By
Login or Register to join the conversation
Peter-P avatar Peter-P 3 months ago

HI, could you add an extra chapter about filters? I don't see how they work with custom resources. Is there an easy way to make it work?

2 | Reply |

Hey @Peter-P!

I'm hoping to do an episode 4 at some point, which will include filters. No timeline however! Until then, we do talk about custom filters in the API Platform v2 version of this tutorial - https://symfonycasts.com/screencast/api-platform2-extending - as far as I know, not so much has changed in this area.

I don't see how they work with custom resources. Is there an easy way to make it work?

But to answer this, yes, when you have a custom resource, you probably need to create custom filters. If you're using our stateOptions + entityClass trick, then you can reuse the normal "entity" filters, which we showed.

If you have any questions along the way, I'd be happy to do my best to answer them.

Cheers!

| Reply |
Evgeny avatar Evgeny 3 months ago

What about API Platform 3 Part 4: Using graphql to the fullest?

1 | Reply |

Thank you for your suggestion @Evgeny we love to know what other topics you'd like to learn, and we consider them when choosing our next tutorial.
Cheers!

2 | Reply |
Alessandro-M avatar Alessandro-M 6 months ago

Yes!!

This is the course I've been waiting.

Looking forward to it! :)

1 | Reply |

It's coming soon!

| Reply |
Jeremy avatar Jeremy 21 days ago

This is a really nice way to design APIs, I really love it! This look so much clearer than having tons of serialization groups everywhere, and having calculated field in dedicated classes is so nicer than having them in entities.

But whoa, if you're going to deal with several entities, you better have a nice AI companion to code with you or it'll be a real pain to write all you Api, EntityToApiMapper and *ApiToEntityMapper.
I'd probably gave up while creating these 3 files manually for each Entity, as it is so time consuming and off-putting.

| Reply |

Hey @Jeremy

Yea, I agree, it feels like a lot to do if you want to rely on DTOs. I hope ApiPlatform will add better support to DTOs in the future

Cheers!

| Reply |
Peter-Hoehne avatar Peter-Hoehne 22 days ago

Hi,
as response from POST and PATCH requests we get the serialized resource,
like when i do a GET request. i noticed that changes to the resource that not coming from my POST/PATCH requests are not updated and reflected in the response. an example is an automatically changed property like "updatedAt". instead there is the old value or nothing at all when the value was not set before.
In the follow up requests i see the changes.
Is there an elegant way to ensure that i get the actual state of the resource?
What i tried so far is:
a) in the Dto2Entity Mapper also setting the Dto properties. It works, but at this point i dont have all infos, for example updatedAt coming from an doctrine event.
b) in the processor where you set the id of the generated entity back to the dto. That works but there have to be some sort of callback/hook mechanism for a generic processor.
c) similar to b) not really tested, but is it a good idea to simply call the micromapper and overwrite the dto by mapping the entity back to it?

Thanks for your great tutorials.
Peter

| Reply |

Hey @Peter-Hoehne!

Yea, I think understand the issue and I've had this conversation in the comments on this tutorial a few times (I wish I could find those!).

The answer is (c). Others have suggested it, and I agree. It also removes the need to manually call $data->id = $entity->getId(); to set the entity id back onto the DTO. The micro mapper will take care of that.

So yea, go for C! There could be a small performance impact, but I bet it's small, and it will only happen on those write operations anway.

Cheers!

| Reply |
MMilev avatar MMilev 2 months ago edited

Hello, a great course :)

I just have one question. Maybe, I have missed it but we saw how to persist embedded objects with Groups but what about the same case with DTOs?

Is it possible to create nested objects via DTOs? For instance I am trying to persist a new User object with liked Profile object (1:1 in the particular case). I am using the microMapper but now I got an error "400 Nested documents for attribute \"profile\" are not allowed. Use IRIs instead.". If I use serialization Groups I do get to save both objects but it does not retrieve any fields, besides the LD+JSON ones @id, @type.

Everything in all the mappers seems OK.

Example Post Request body:

{
  "email": "test-email@example.com",
  "password": "123456",
  "profile": {
    "firstName": "FirstName",
    "middleName": "MiddleName",
    "lastName": "LastName"
  }
}
| Reply |
MMilev avatar MMilev MMilev 2 months ago edited

I have found the culprit. I use the suggested EntityClassDtoStateProcessor but it does not populate the ID field for all the embedded objects, thus generateIriFromResource throws an exception. In the code bellow the commented line actually makes it work for the discussed case.

EntityClassDtoStateProcessor#process
...

$this->persistProcessor->process($entity, $operation, $uriVariables, $context);
$data->id = $entity->getId();
// $data->profile->id = $entity->getProfile()->getId();

return $data;
| Reply |

Hey @MMilev!

Ah yea, that makes sense. Some people, which I think makes sense, have been using the micromapper after calling $this->persistProcessor->process() to map the $entity back into the DTO. Because the $entity (and in your case the Profile) now have ids, this maps the ids back to the DTO's.

Cheers!

2 | Reply |

Hello, thank you for your answer :) I will try that :)

Best regards :)

| Reply |

Delete comment?

Share this comment

astronaut with balloons in space

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