> APIs >

Course Overview

API Platform 3 Part 3: Custom Resources

Become an API power user with our new tutorial on Symfony. Learn state providers, custom fields, data transformation, and more.

  • 987 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!

17 Comments

Sort By
Login or Register to join the conversation
Peter-P avatar Peter-P hace 5 meses

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 hace 5 meses

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

2 | 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 hace 8 meses

Yes!!

This is the course I've been waiting.

Looking forward to it! :)

1 | Reply |

It's coming soon!

| Reply |

Hello,
Thanks for the Course.
I think there might be a bug in the chapter 36 of this part 3.
In the src/Validator/TreasuresAllowedOwnerChangeValidator.php file, there is a return statement in the loop :

foreach ($value->dragonTreasures as $dragonTreasureApi) {
    assert($dragonTreasureApi instanceof DragonTreasureApi);
    $originalOwnerId = $dragonTreasureApi->owner?->id;
    $newOwnerId = $value->id;
    if (!$originalOwnerId || $originalOwnerId === $newOwnerId) {
        return; // <-- HERE !
    }
    // the owner is being changed
    $this->context->buildViolation($constraint->message)
        ->addViolation();
}

I think this will be problematic when configuring multiple IRIs. Would't that check only the first DragonTreasureApi instance ? then if the first is owned by the user, the others won't be checked ?
As it is far in the course, the problem might come in earlier chapters.

In a completely other subject, I saw you talked about releasing a new episode. It might be good to see how to handle file upload, I followed the official documentation and I had a really difficult time to set it up ;)

| Reply |

Hey @fGuix

That's a good observation. The validator will stop if the DragonTreasureApi object has no owner (a new object has been created), or if the owner changed. But, as you said, there's a tiny issue with this code, it won't check all objects. What I can say is Ryan assumed that all the modified objects belong to the same owner. It can be seen as a bug or not but it can be easily fixed by changing the return statement by continue

Cheers!

| Reply |

Hello @fGuix,

Nice catch, the case looks legit. The best thing to bypass this issue I think will be to change return; to continue;. We will discuss internally how to fix the course properly.

Cheers!

| Reply |
Jeremy avatar Jeremy hace 2 meses

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 hace 2 meses

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 hace 4 meses 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 hace 4 meses 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!