Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

API Platform Part 3: Custom Resources

5:13:28

What you'll be learning

This tutorial also works great with API Platform 2.6.
// 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
    }
}
// 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
    }
}

Welcome to part 3 of the Api Platform series! In part 1, we built a fully-featured API. Then in part 2 we leveled-up by adding a robust security system, security checks and user-specific fields.

So what's left? In part 3, we're taking customizations to the next level:

  • A RESTful implementation of a "publish" action
  • Complex ACL rules around who can publish under different conditions
  • Doing custom actions before save with a data persister
  • Truly custom fields via a data provider
  • Completely-custom, non-entity API resources
  • Adding pagination to custom resources
  • Custom filters (for entity and non-entity resources)
  • The input/output DTO system
  • Using UUID's

Phew! Yep, in this tutorial, we're going deep into customizations. Ready? Let's go!


Your Guides

Ryan Weaver

Buy Access
Login or register to track your progress!

Join the Conversation?

42
Login or Register to join the conversation
Default user avatar
Default user avatar Gianluca | posted 1 year ago

Hi,
I have to get some entities from database, but if these entities are missing, I have to fetch them from an external API and, then, before return in the response, I have to save them in DB . In the response, we always see the entity list.
I've decided to create a EntityXDataPersister and a EntityXDataProvider.

If the collection is empty, I call the Api ( injected within the constructor of DataProvider ), I persist them using EntityXDataPersister ( inhected in the constructor of DataProvider ) ... then I return the collection.

What do you think about this way to proceed? Do you see something wrong with Api Platform lifecycle?

Thanks in advance

1 Reply
Default user avatar
Default user avatar Gianluca | posted 1 year ago

Hi,
which is the main different between DataPersister and Event Listner? I mean, let I have to "populare" some fields based on some conditions, ie, if the user is an admin , autofill some Entity Fields.

I can do this using DataPersister, but I can also add an EventSubscriber, as shown here:

https://api-platform.com/do...

I can listern the event kernel.view, with constant PRE_WRITE, probabply the result is the same.

Which is for you a discriminant we can use to choose between dataPersister and event system?

1 Reply
Titoine Avatar

Hi,
Will there be an update with ApiPlatform v3?
The whole part with DataTransformer is out of date as they are removed from V3.

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Titoine | posted 10 months ago

Howdy Titonine,

Yes, this is something that is planned. But there is no definitive date as to when it will be available.

Reply
Default user avatar
Default user avatar Gianluca | posted 1 year ago

Hi,
is there a way to use an OutputDTO for collectionOperations? which is the best way to show an output different from a simple listing?
Thanks

Reply
Jonas D. Avatar
Jonas D. Avatar Jonas D. | posted 1 year ago

Hi, im having an issue with the source files.
Class Doctrine\Common\Cache\ArrayCache does not exist.

Ive searched a bit for a solution, but no success.
I tried to change doctrine version, php version without change.

Any help would be apreciated!

Reply

Hey there,

Doctrine has removed their cache implementation from their source code, you should use Symfony's cache or any other caching service. Cheers!

Reply
Kiuega Avatar

Hello here ! I just installed api-platform-admin ! It's just... awesome <3 Perfect dashboard http://image.noelshack.com/...

I added it to the application that we are developing on courses 1, 2 and 3 (and soon 4 if I understand correctly), with React.

I don't know if this is planned in part 4, but maybe you should quickly make a video on it. I find this super useful, especially during development. It kind of replaces EasyAdmin and it's great!

On the other hand, the api-platform client generator is more complicated to install. Perhaps a video on it would also be welcome, for a client part on some resources!

Reply

Hi Kiuega!

I'm happy to hear you liked ApiPlatform Admin! Yeah, it's on our TODO to show it in a course. I'm not sure if it will be included in ep4 of Api Platform series or in another separate course, we don't have any certain plan about it yet, but I may assure that we're going to cover this some day.

We also have plans to cover ApiPlatform client generator as well.

If you have some more specific topics about ApiPlatform Admin or Client Generator that you experience problem with and could share with us - it would help!

Thank you for your interest in SymfonyCasts tutorials and your patience!

Cheers!

1 Reply
Kiuega Avatar

Awesome ! Regarding API Platform Part 4, do you plan to release it in 2021?

Reply

Hey Kiuega,

I'm not sure it may happen in 2021, we have a lot of other good stuff in progress that will be released first. So, most probably it will happen in 2022, but that's a very rough estimation as we don't plan so far from now yet.

Thank you for your patience!

Cheers!

1 Reply

Any new insight into this estimation? Keep up the awesome work guys!!

Reply

Hey jmwamser!

Not right now unfortunately - we've got a bunch of videos that would be in front of it, so probably not for awhile at best.

Sorry I can't give you a better answer!

Cheers!

Reply
Richard H. Avatar
Richard H. Avatar Richard H. | posted 2 years ago | edited

Hi, I see you switched to UUIDs in this tutorial.

It looks like, that if you add an "id" to a POST request, API Platform handles the request as PUT request. The error message is "Update is not allowed for this operation.".

There is an issue (https://github.com/api-platform/api-platform/issues/343) but with no solution -- actually sending "Content-Type: application/ld+json" instead of plain json fixes is. Cumbersome, no?

So: how come that your test testCreateUserWithUuid is not failing?

Reply

Hey @Richard!

Ha! You've been looking ahead - at the final code in the download? I love it!

Yea, you're right about this issue - and I totally hit it. To be honest, I don't really solve it - I show the problem and the 2 work arounds:

1) The first workaround is to call the field "uuid" instead of id. Done, it works :p.

2) The second workaround is to send a Content-Type header set to application/ld+json, because actually, the bug is with the "json" denormalizer system, but not the json-ld denormalizer system. This is a fine work around if you are the only person using your API (you don't have 3rd party users). One way to avoid the header (it's really another workaround) is to remove the "json" format entirely so that json-ld becomes the default, or to add a listener that effectively makes json-ld the default instead of json. That basically enforces json-ld as the "input" format.

So... not really solved - that's an annoying issue. But I hope this helps!

Cheers!

Reply
Default user avatar

This helps. Thanks! I guess it is save to remove the json format for input since it does not really differ from json+ld for the input :)

Reply
Anton S. Avatar
Anton S. Avatar Anton S. | posted 2 years ago

Hello!
I want have a light query to get elements count in table, for example /users/count. How i can make it? I know about hydra:totalItems in collectionOperations but i think select all elements it is heave query, pls help me

Reply

Hey @Антон Степанов!

Hmm, interesting situation! So you absolutely *could* create a custom "operation" for this and make something like /users/count - that would be *fine*. I would do a manual, quick count query yourself in that operation and return it. It's not very RESTful... but who cares ;).

I can think of a few other, more RESTful ways to do this... but ultimately, they all still query for objects in the background... so the payload would be smaller, but our app is still doing some work to query for objects, even though you only need the COUNT. So... I'm trying to think if I'm not thinking of something... but I think the best way is a custom operation.

Cheers!

Reply

Hey,

you can also add this course in APIs track (like parts 1 and 2) :D

Thanks

Reply

Hey Steven!

Duh! Thanks for the reminder - it's there now!

Cheers!

Reply
Dmytro K. Avatar
Dmytro K. Avatar Dmytro K. | posted 2 years ago | edited

Hey)
Really waiting for this course =)

But I need some help, or maybe this can be an idea for the next tutorials...

Is it possible to send the custom messages for the post creation for example?

For example, the post was created (or not) and I can get an API response like this:

`
{
"@context": "string",
"@id": "string",
"@type": "string",
"id": "string",
"messages": {

    "Your post was saved or any other information from controller/annotation for the current action"

},
.... rest of the entity
}
`

In idea, I want to have custom messages after some logic in the controller for example, etc.

Or should I use the Symfony messenger, use it with the event system, and listen the sockets on the frontend app?

Reply

Hey back Dmytro K.!

So... the "problem" (which I put in quotes because... ultimately, you can do whatever you want) is that this isn't really RESTful - you're attaching an extra key to your response that isn't actually related to the ApiResource. But, we CAN totally do this, here are a few options:

1) Add a non-persisted "messages" field to your entity and exposer it in your API. The nice thing is that this really becomes part of your API. It IS still a bit hacky, because this field will change on different operations, but it's at least documented. You could also choose to only expose this field (through serialization groups) on some operations (maybe on POST but not on GET). Then, create a custom data persister (that's the first thing we do in this tutorial) that performs the "custom logic" and then sets this messages property.

2) Another option might be to do the Messenger route, which we won't talk about until the next tutorial. In that case, your handler could return a value and that value will be serialized. I would probably create & return an object of a class that has a "messages" property and also a "post" property that contains the entity object. Then, when ApiPlatform serializes this, it would have a messages key and a post key, which is an embedded object.

These are - more or less - the types of things (minus the Messenger integration) that we'll talk about in this course. Everyone has a different "weird thing" that they want to do :p.

> Or should I use the Symfony messenger, use it with the event system, and listen the socket on the frontend app?

This is definitely another option - it DOES kind of seem like you want to perform a POST request... and then that POST request may or may not create "messages" that the user should be notified about. Using something like Mercure and listening to those messages on the frontend would be a more pure/fancy approach.

Cheers!

Reply
Vladislav T. Avatar
Vladislav T. Avatar Vladislav T. | posted 2 years ago

I watched the first and half of the second courses. I am very excited about API Platform features. But I am concerned about the ability to create custom endpoints. For example, if I need a nested menu tree - is it possible to create such one with API Platform (with auto-generated documentation, etc)? Is the topic "
Completely-custom, non-entity API resources" about something like this?

Reply

Hey Vladislav T.!

> I watched the first and half of the second courses. I am very excited about API Platform features.

Yaaay!

> But I am concerned about the ability to create custom endpoints.

This is one of the big topics that we haven't covered, and we will only *partially* cover it in this course (but I will cover it more in a part 4 that I plan to release as quickly as possible - it was actually part of this tutorial, but it got so big that I split this part 3 into parts 3 and 4).

Anyways, can you tell me more about your use-case? It sounds like you have a resource with a self-referencing relationship. What would the custom endpoint(s) look like that you need? Let me know - we can still impact some of the tutorial's contents.

Cheers!

Reply
Vladislav T. Avatar

> Anyways, can you tell me more about your use-case? It sounds like you have a resource with a self-referencing relationship.

Yes, in my case - it is trees (nested arrays with various nesting levels). For example menu or other resources with children which have there own children and so on.

Reply

Hey Vladislav T.~

Ok! And, what custom operations do you have "envisioned" for these? For example:


POST /api/menu-items

{
    "name": "Products",
    "children": [
        { "name": "Bikes" },
        { "name": "Games" }
    ],
}

Where this would create a top-level menu item and also create 2 new children. This is just an example - I'm trying to see how you want the API endpoints to look :).

Cheers!

Reply
Vladislav T. Avatar
Vladislav T. Avatar Vladislav T. | weaverryan | posted 2 years ago | edited

Exactly. Create and receive nested data with various nesting levels.

POST /api/menu-items/

or

GET /api/menu-items/


{
  "name": "Products",
  "children": [
    {
      "name": "Bikes",
      "children": [
        {
          "name": "Bikes 2",
          "children": [
            {
              "name": "Bikes 2-1"
            },
            {
              "name": "Bikes 2-2"
            }
          ]
        },
        {
          "name": "Bikes 3"
        }
      ]
    },
    {
      "name": "Games"
    }
  ]
}
Reply
Default user avatar
Default user avatar Андрей Селин | weaverryan | posted 2 years ago | edited

Exactly. Create and receive nested data with various nesting levels.
POST /api/menu-items/
or
GET /api/menu-items/


{
  "name": "Products",
  "children": [
    {
      "name": "Bikes",
      "children": [
        {
          "name": "Bikes 2",
          "children": [
            {
              "name": "Bikes 2-1"
            },
            {
              "name": "Bikes 2-2"
            }
          ]
        },
        {
          "name": "Bikes 3"
        }
      ]
    },
    {
      "name": "Games"
    }
  ]
}
Reply

Hey Андрей Селин!

I've not played with recursive relationships like this with ApiPlatform, but I kind of think that this should "just work". If you have a MenuItem ApiResource and it has a "children" property, as long as you exposed the children property via the correct serialization group, then I think it would recursively serialized in a similar way to this.

The same is true when creating them: as long as all the fields have the correct deserialization group (so that they're writable), I think you could send a POST request with embedded menu items and all of them would be created and linked together.

Of course, this is complex, so I could be wrong. Have you tried this before? You may need to tweak some "eager loading" settings - you can see that here: https://github.com/api-plat...

Cheers!

Reply
Vladislav T. Avatar

Hi, parent-child relations work as expected. But I understood what kind of endpoints I interested. For example what about bulk actions (delete all records checked by ckeckboxes) ?

Reply

Hey @Андрей Селин!

Ah, an excellent question! To be honest, I've never thought about bulk operations before! There is an issue about it here - https://github.com/api-platform/core/issues/1482 - which uses a custom operation. This looks valid, though I'm not sure if there might be a better solution. My initial thought would be to create a totally custom API resource class with only one operation (a post operation for something like /api/products/bulk) and probably no output. The custom class would be something like this:


// the normal @ApiResource annotations, but only 1 operation
class ProductBulk
{
    /**
     * @Groups("whatever groups you need")
     * @var array<Product>
     */
    public $products;
}

This would all you to POST to the URL with a products key containing an array of product IRI strings. Then you'd create a custom data persister that would receive this object and do whatever you wanted. Heck, you could make this class look however you want.

This is vague answer... as I'm making it up as I type ;). Let me know what questions you have - this might be a good thing to cover in a future tutorial.

Cheers!

Reply
Default user avatar

Hi,

Can you please make a tutorial of a bulk operation to post multiple resources for an entity ?

1 Reply

Hey @Kate!

We will (at some point) make one more tutorial covering a few more custom things. I've added this idea to our list!

In general, I would probably implement this with either the Messenger integration or a custom controller. It is not, as far as I know, a very "RESTful" thing to do, which is fine, but that's probably why it doesn't feel like it fits with a normal, RESTful solution.

Cheers!

Reply

I support the idea of doing a tutorial on bulk operations... tasks like Mark all as readed or unreaded, delete a group of items, etc... those are really common tasks!! And with your skills at teaching @weaverryan this will be a really nice tutorial ;) hahaha... I can't wait!! Cheers!!

Reply

Hey @weaverryan! I was wondering... It's this approach valid to change the status of a resource in some flag field for example a group of notifications from readed to unreaded and then some Cron task that run some symfony command that will change the status in the background... But I really don't know how to give a feedback to the front end because like you suggest this endpoint should not have output (probably...😅🤷‍♂️) . But I really don't know if this solutions is really good because for example change the status of 200 notifications it will take some time... So I'm really confused about what to do in the controller of this endpoint... Could you give me some tips on this!?

Reply

Hey Toshiro!

But I really don't know how to give a feedback to the front end because like you suggest this endpoint should not have output (probably...😅🤷‍♂️) . But I really don't know if this solutions is really good because for example change the status of 200 notifications it will take some time

In this case (or any case where it may actually take some rime to do the work you need to do), I would leverage Symfony Messenger. API Platform has direct Messenger support, but that's less important: you could use that or just create your own custom message class in your controller and dispatch it to Messenger. Either way, what you would do is create, for example, a MarkMessagesAsRead message class that probably has a public function __construct($messageIds) constructor. You would dispatch that to Messenger, and allow it to be handled by a worker (your example of doing this with Cron is also fine... it's just that this is exactly what Messenger is meant to do already!).

Finally, regardless of how you set up your system to "do work later", in your API endpoint, you would respond with... really whatever you want. Technically, 202 is common for this. It means "I'm done... but really the work will be done later". I think the big problem for you (and tell me if I'm wrong) is that "doing this work later" is challenging for your user interface. After all, if the user, for example, reloaded the page... and you made a fresh API call for the user's notifications, those 200 notifications might still look "unread". That's... kind of a non-API-related challenge. You either need to be "ok" with this (and maybe you at least try to fake it by not showing the 200 notifications... though maybe they will show back up if the user reloads) or find a faster way to at least mark them as read. For example, you could make. a really quick UPDATE notification SET status='read' WHERE id in (1, 2, 3, 4, ...) type of query during your API call. Then, it would be near instant. If you need to do extra processing (maybe when a notification is read, you also need to delete some file stored somewhere), you could STILL create and dispatch a MarkMessagesAsRead message. It wouldn't actually mark the messages as read, but it would do whatever "other" work is needed in the background to fully finish the process.

Let me know. if that helps!

Cheers!

Reply
Vladislav T. Avatar

Thank you! I am very interested how far Api-platform endpoints customization can go.

Reply
It O. Avatar

can't wait

Reply

Hey Alberto,

We too! It's very closed to be released, some final video editing :)

Thank you for your patience!

Cheers!

Reply
Miky Avatar

Hi, will great if you will show us the way with UIID and how to parse it into API.. because many corporate applications use uiid instead primary key...

Reply

Hey Miky!

Hmmm, that's a cool idea! I'm still recording this, so I'll see if I can squeeze this topic in :).

Cheers!

1 Reply
Stefan T. Avatar
Stefan T. Avatar Stefan T. | posted 2 years ago

Courses with parallel testing was good idea, keep doin.

Reply
Cat in space

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

userVoice