Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Operations

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's get to work customizing our API. A RESTful API is all about resources. We have one resource - our CheeseListing - and, by default, API Platform generated 5 endpoints for it. These are called "operations".

Collection and Item Operations

Operations are divided into two categories. First, "collection" operations. These are the URLs that don't include {id} and where the "resource" you're operating on is technically the "collection of cheese listings". For example, you're "getting" the collection or you're "adding" to the collection with POST.

And second - "item" operations. These are the URLs that do have the {id} part, when you're "operating" on a single cheese listing resource.

The first thing we can customize is which operations we actually want! Above CheeseListing, inside the annotation, add collectionOperations={} with "get" and "post" inside. Then itemOperations with {"get", "put", "delete"}.

Tip

Starting in ApiPlatform 2.5, there is also a patch operation. It works like the put operation and is recommended over put when you only want to change some fields (which is most of the time). To allow the patch operation, add this config:

// config/packages/api_platform.yaml
api_platform:
    patch_formats:
        json: ['application/merge-patch+json']

Then, when making a PATCH request, set the Content-Type header to application/merge-patch+json. Check the interactive documentation, you'll see an example there ;).

... lines 1 - 7
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "delete"}
* )
... line 13
*/
class CheeseListing
... lines 16 - 116

A lot of mastering API Platform comes down to learning about what options you can pass inside this annotation. This is basically the default configuration: we want all five operations. So not surprisingly, when we refresh, we see absolutely no changes. But what if we don't want to allow users to delete a cheese listing? Maybe instead, in the future, we'll add a way to "archive" them. Remove "delete".

... lines 1 - 7
/**
* @ApiResource(
... line 10
* itemOperations={"get", "put"}
* )
... line 13
*/
... lines 15 - 116

As soon as we do that... boom! It's gone from our documentation. Simple, right? Yep! But a bunch of cool things just happened. Remember that, behind the scenes, the Swagger UI is built off of an Open API spec document, which you can see at /api/docs.json. The reason the "delete" endpoint disappeared from Swagger is that it disappeared from here. API Platform is keeping our "spec" document up to date. If you looked at the JSON-LD spec doc, you'd see the same thing.

And of course, it also completely removed the endpoint - you can see that by running:

php bin/console debug:router

Yep, just GET, POST, GET and PUT.

Customizing the Resource URL (shortName)

Hmm, now that I'm looking at this, I don't love the cheese_listings part of the URLs... API Platform generates this from the class name. And really, in an API, you shouldn't obsess about how your URLs look - it's just not important, especially - as you'll see - when your API responses include links to other resources. But... we can control this.

Flip back over and add another option: shortName set to cheeses.

... lines 1 - 7
/**
* @ApiResource(
... lines 10 - 11
* shortName="cheeses"
... lines 13 - 14
*/
... lines 16 - 117

Now run debug:router again:

php bin/console debug:router

Hey! /api/cheeses! Much better! And we see the same thing now on our API docs.

Customizing Operation Route Details

Ok: so we can control which operations we want on a resource. And later, we'll learn how to add custom operations. But we can also control quite a lot about the individual operations.

We know that each operation generates a route, and API Platform gives you full control over how that route looks. Check it out: break itemOperations onto multiple lines. Then, instead of just saying "get", we can say "get"={} and pass this extra configuration.

Try "path"= set to, I don't know, "/i❤️️cheeses/{id}".

... lines 1 - 7
/**
* @ApiResource(
... line 10
* itemOperations={
* "get"={"path"="/i❤️cheeses/{id}"},
* "put"
* },
... line 15
* )
... line 17
*/
... lines 19 - 120

Go check out the docs! Ha! That works! What else can you put here? Quite a lot! To start, anything that can be defined on a route, can be added here - like method, hosts, etc.

What else? Well, along the way, we'll learn about other, API-Platform-specific stuff that you can put here, like access_control for security and ways to control the serialization process.

In fact, let's learn about that process right now! How does API Platform transform our CheeseListing object - with all these private properties - into the JSON that we've been seeing? And when we create a new CheeseListing, how is it converting our input JSON into a CheeseListing object?

Understanding the serialization process may be the most important piece to unlocking API Platform.

Leave a comment!

27
Login or Register to join the conversation

Hello,

If you are using Symfony 5 with PHP 8, you can use attributes :


#[ApiResource(
collectionOperations: ['get', 'post'],
itemOperations: [
'get' => [
'path' => '/i❤️cheeses/{id}'
],
'put'
],
shortName: 'cheeses'
)]

1 Reply

Sweet! Thanks for sharing it

Reply
David-G Avatar

Hi everybody,
I cant understand a small things
When i add a path in the itemOperations :
for example :

#[ApiResource(
    itemOperations: [
        'get' => ['path' => '/cheese/{id}']
    ],
    shortName: 'cheeses'
)]

And i go to the url : /api/cheese/1.json
i don't have the json format on screen, its the API Platform html screen.

But if i don't define a custom path i have the json format on screen.

I hope i explain this problem correctly.

Does anyone have an explanation to this ?

Thanks :)

Reply

Hi @David-G!

Yes, I know this issue! To fix it, change the path to /cheese/{id}.{_format}.

By default, THIS is how Api Platform creates routes - you can see it if you run bin/console debug:router. So if you override the path, it becomes your responsibility to add this. It's a little "gotcha", but easy to fix once you understand the issue :).

Cheers!

1 Reply
David-G Avatar

thank you !

Reply
Dang Avatar

In my ApiPlatform it shows 6 endpoint instead of 5 in the video. The new's one is PATCH. When I document this (if I understand correctly), PATCH is for modifying one property while PUT is for the entire Entity. But the exemple for Put in next video doesn't show this: It modify one property and stills work. So my question is: I will use PUT or PATCH for the update entity?

Reply

Hey Dang !

Yes, there is a new PATCH, I think it's new in Api Platform 2.5. The explanation of what's going on can be found here: https://github.com/api-plat...

I have not worked with PATCH yet, but here is my understanding:

A) Currently PUT works "incorrectly" per the official spec. It, as you correctly said, allows you to modify one property at a time... even though it *should* require you to pass *all* the properties (i.e. a "replace"). Now that PATCH is supported, it appears that, in future versions of API Platform, PUT will change to be a "true" PUT.

B) So, to answer your question: you should probably start using PATCH for these "partial" updates right now (I'll add a note about this). When you do this, you *may* need to set the Content-Type header on your request to application/merge-patch+json, but I'm not sure if that's a requirement - I need to play around with that to be sure :).

Cheers and thanks for the poke to add a note to the video about this for other users!

Reply
Sam Z. Avatar

I am trying to customise the request body for a PUT operation, how do I change the request body description in my code?
Current description is: "The updated cheeses resource"

Reply

Hey Sam Z.

Do you mean how to change the description of a field only for the PUT operation? Perhaps there's a way to do it by configuring the Swagger context https://api-platform.com/do...

Cheers!

Reply
Steven J. Avatar
Steven J. Avatar Steven J. | posted 2 years ago

defautl utf8 has been deprecated, if you use sf >= 4, setting the option is required:
{"path"="/i💚cheeses/{id}", "options"={"utf8":true}}

Reply

Hey Steven J.!

Ah, good tip! I think you can also turn on utf8 globally in the config - https://github.com/symfony/... - it's a default option you get in your routing file if you started a new project today :).

Cheers!

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

How do you get a resource without using a primary key?

ex) /api/units/{otherId}

Reply

Hey Yoshiki T.

That's a good question and there are a few options:
A) Add a filter to your /api/units endpoint that will match by the field you want
B) Change the resource identifier to the field you want
C) Create a custom IriConverter the easiest way is to use the decorate pattern

Cheers!

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

Hi, how can I add a route to a Controller instead of an Entity?

Reply

Hey Jelle S.

You can still add routes to Controllers as normal but it's not recommended. You can read more info about it in the docs: https://api-platform.com/do...

Cheers!

Reply
hous04 Avatar

Hi,

I'm used to working with Symfony to build traditional websites. I have started to learn API platform but I have many questions to ask because I am very confused.

Let's take an example of a traditional website to post adverts, this is how I organize it:

- An administrator can post and manage all adverts (add, edit, delete, list) via admin space.
- A member can also add, edit, delete and list his adverts only through his private space.
- A visitor can view the list of adverts and view the details.

I'm used to make 3 controllers for the Advert entity:

AdvertController in App\Controller\Admin which contains the CRUD and routes for the admin part
AdvertController in App\Controller\Front\MemberSapce which contains the CRUD and the routes for the member space
AdvertController in App\Controller\Front\PublicSpace which contains the CRUD and the routes for the visitor part

and I give the names of the routes like this:

- admin_advert_index , admin_advert_new , admin_advert_edit ....
- member_space_advert_index , member_space_advert_new, member_space_advert_edit ....
- front_advert_index , front_advert_show ...

When I started to learn API platform, I understood that there are 4 or 5 main operations (item and collections), but I didn't understand how to create other specific operations for each part (admin, front , member space)...

How to well organize my project, how for example an admin can manage all the adverts via admin space (not easyAdmin), how a member can manage only his own adverts via his private space. How we can create these all spaces ... Should we also create 3 others controllers like I'm used to do to build traditional website? If yes, so can a controller contain the CRUD or we should create a single class (persister I think) for each function ?

Really I am very confused and things are not clear. Can you help me and explain us how to organize a project with API platform?

I will be very grateful.

Thank you.

Reply
hous04 Avatar

Because I have never created an API Rest and because this is the first time I'd like to ask you as you are an experienced developer.

What I'd like to say is, imagine that we have an item opreration "get" to retrive the Advert data and we have 3 spaces in my application (administration, MemberSapce and public) and the frentend developer who will create these 3 interfaces.

So does the frentend developer will consume the same operation (get) above to dispaly the advert data in each space and the backend developer uses the "groups" to tell who can show this attribute and can't show it ? Or I should have 3 routes and 3 operations ?

I'd like to undrestand this point to continue learning API platfrom and also because I have never seen how a frentend developer consume an API.

Thank you

2 Reply

Hey hous04 !

Welcome to the very-different world of APIs :p. Let me do my best to explain how *I* would handle this situation.

In short, even if I will have 3 different interfaces for my "Advert" data, I would only have 1 "ApiResources". That's not an absolute rule, but let's "start" with this :). From a data perspective, let's think about the 3 "interfaces" an how each might be different:

A) The "admin" interface will probably contain a list of "all" Adverts - not just the adverts "owned" by a specific user.
B) Some interfaces may "allow" extra fields to be displayed or set - e.g. in the "admin" interface, you may render "extra" data that normal users can't see or have extra fields that a normal user can't input.
C) The "member" interface probably also renders some data that is not rendered on the "public" space.

I think that all of these interfaces can be built from the same "Api Resource" via groups or custom normalizers (depending on the situation). For example, if you are logged in as an admin, then the Advert "collection" operation would return *all* Adverts. So naturally, if you are looking at the "admin" interface (which you will only allow people to access if they are an admin), this page will list ALL Adverts. The API endpoint would maybe also return "extra" admin-only fields because the user is an admin, which the frontend would display.

The only weird part is that if the admin THEN goes to their "member" interface and the JavaScript makes a GET request for the collection endpoint, it would return ALL Adverts, not just their *own* adverts. That could be solved with an "exact" SearchFilter - https://symfonycasts.com/sc... - the frontend would add something like ?owner=/api/users/5 to the URL inside the "member" interface to filter to only "my" adverts. If a "smart" user tried to hack this and change it to "/api/users/6" (some other user), you will already have ACL logic inside your system to prevent this - and it will return an empty set.

Also, if an admin goes to their "member" interface, the API may return "extra" fields or allow extra fields to be "submitted" because they are an admin. That's fine: the interface simply won't render that extra data or create "fields" to allow that data to be set. It's just "extra stuff" that the interface ignores.

For the "public" interface, you would probably want to display all... maybe "published" adverts or something. Again, I would use a filter and make the interface add a "?published=1" query parameter when you fetch the collection of them. If a "smart" user removed that flag to try to "hack" the system, you would (once again) already have logic in place to limit them to only view the adverts that their user level allows (an admin would see all adverts and a normal user might see all public adverts + private adverts that they own).

Does that make sense? There are 2 critical pieces to this:

1) Make sure you have a security layer that (on matter what) only allows people to list/show/edit the exact adverts that they should have access to. And also, a security layer that (no matter what) only allows people to "write" the exact fields on Advert that they should be allowed to write (and the same for "read").

2) Then, use filters to "allow" front-end developers to "limit" the list (and maybe even "fields" if you care enough - via sparse fieldsets) in whatever way they need to in order to create the interfaces they want.

Let me know if it helps! You *may* still need custom "actions" or other custom things - like maybe if you need a special endpoint to "publish" an Advert - but that's a separate conversation from "how do I use 21 ApiResource to power 3 interfaces. And if your interfaces are VERY different, you could also *decide* to have one ApiResource per interface if you want. This would not mean 3 Doctrine entities: it would mean creating 3 "model classes" (one for each interface) and then using a custom data persister, etc to populate that manually from your Advert data. This is a bit more advanced and may not be needed.

Cheers!

Reply
hous04 Avatar

Hi
Yes , it's "very-different world" :p , it's clear now and I will try to apply your advices.

Thank you

Reply

Hi, how did you made that heart appear with /i?> cant get it

Reply

Hey Tadis

That "heart" icon is an utf8-mb4 emoji. If you are using PHPStorm you can install this plugin https://plugins.jetbrains.c... and insert emojis into your code very easily.

Cheers!

Reply

Thanks so much Diego :)

Reply
Andrey S. Avatar
Andrey S. Avatar Andrey S. | posted 3 years ago

One thing is not clear for me after this video. If I need to create custom action e.g. I need to unpublish post and do many many different operations during this operation (delete comments, send email for subscribers, reindex ES, invalidate cache etc.).

So it is not patch for "/post/145" with "is_published": 0 it is big action with independent logic.

I would like to have /post/145/unpublish or something like this which will be resolved to PostController::unpublish where I will do all needed operations and output the same data as standard PATCH method.

I saw on the page https://api-platform.com/do... how to make custom operation for controller, but how to force api-platform to do docs, output etc. same as for PATCH?

Reply

Hey Andrey S.!

Such a simple question, and yet this is such a loaded topic :). It's something we'll cover in a future tutorial. BUT, I'll give you some hints (but also, there is not one perfect answer). Creating a URL like /post/145/unpublish is not RESTful. So, you're "breaking" the rules... which you should avoid if you can... but also... at some point - damn the rules and get your job done. So yes, this is one possible approach - but I would make it POST (instead of PATCH) - just because POST tends to be the best option (meaning, the one that follows the rules the "most") for these non-RESTful endpoints, imo.

Anyways, I have not played much with custom operations (as it's something we're covering later), but if you follow the docs about custom operations - https://api-platform.com/do... - does this not generate any API docs? Let me know :).

Cheers!

Reply
Ioan adrian S. Avatar
Ioan adrian S. Avatar Ioan adrian S. | posted 3 years ago

I could not find shortName in official documentation. From where it comes?

Reply

Hey Adrian,

Yeah, it sounds like missing feature in docs, probably they didn't want to complicate example with annotations. I found this shortName for Yaml/XML mapping only on this page: https://api-platform.com/do... - though their search does not find it either, I searched with Google instead.

Cheers!

Reply
Cat in space

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

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
    }
}