If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribePongamos manos a la obra para personalizar nuestra API. Una API RESTful se basa en recursos. Tenemos un recurso -nuestro CheeseListing
- y, por defecto, la Plataforma API ha generado 5 rutas para él. Estos se llaman "operaciones".
Las operaciones se dividen en dos categorías. En primer lugar, las operaciones de "colección". Son las URL que no incluyen {id}
y en las que el "recurso" sobre el que operas es técnicamente la "colección de listados de quesos". Por ejemplo, estás "obteniendo" la colección o estás "añadiendo" a la colección con POST.
Y en segundo lugar, las operaciones de "artículos". Estas son las URL que sí tienen la parte {id}
, cuando estás "operando" sobre un único recurso de listado de quesos.
Lo primero que podemos personalizar es qué operaciones queremos realmente Por encima deCheeseListing
, dentro de la anotación, añade collectionOperations={}
con"get"
y "post"
dentro. Luego itemOperations
con {"get", "put", "delete"}
.
Tip
A partir de ApiPlatform 2.5, también hay una operación patch
. Funciona como la operación put
y se recomienda sobre put
cuando sólo quieres cambiar algunos campos (que es la mayoría de las veces). Para permitir la operación patch
, añade esta configuración:`
// config/packages/api_platform.yaml
api_platform:
patch_formats:
json: ['application/merge-patch+json']
... lines 1 - 7 | |
/** | |
* @ApiResource( | |
* collectionOperations={"get", "post"}, | |
* itemOperations={"get", "put", "delete"} | |
* ) | |
... line 13 | |
*/ | |
class CheeseListing | |
... lines 16 - 116 |
Gran parte del dominio de la Plataforma API se reduce a aprender qué opciones puedes pasar dentro de esta anotación. Esta es básicamente la configuración por defecto: queremos las cinco operaciones. Así que no es de extrañar que, cuando actualizamos, no veamos ningún cambio. Pero, ¿qué pasa si no queremos permitir a los usuarios eliminar un listado de quesos? Tal vez, en lugar de eso, en el futuro, añadamos una forma de "archivar" los mismos. Eliminar "delete"
.
... lines 1 - 7 | |
/** | |
* @ApiResource( | |
... line 10 | |
* itemOperations={"get", "put"} | |
* ) | |
... line 13 | |
*/ | |
... lines 15 - 116 |
En cuanto hagamos eso... ¡boom! Desaparece de nuestra documentación. Sencillo, ¿verdad? ¡Sí! Pero acaban de ocurrir un montón de cosas geniales. Recuerda que, entre bastidores, la interfaz de usuario de Swagger se construye a partir de un documento de especificaciones de la API abierta, que puedes ver en /api/docs.json
. La razón por la que el punto final "eliminar" desapareció de Swagger es que desapareció de aquí. La Plataforma API mantiene actualizado nuestro documento de "especificaciones". Si miraras el documento de especificaciones JSON-LD, verías lo mismo.
Y, por supuesto, también ha eliminado por completo la ruta -puedes comprobarlo ejecutando:
php bin/console debug:router
Sí, sólo GET
, POST
, GET
y PUT
.
Hmm, ahora que lo veo, no me gusta la parte cheese_listings
de las URLs... La Plataforma API la genera a partir del nombre de la clase. Y realmente, en una API, no deberías obsesionarte con el aspecto de tus URLs, no es importante, especialmente -como verás- cuando tus respuestas a la API incluyen enlaces a otros recursos. Pero... podemos controlar esto.
Vuelve a dar la vuelta y añade otra opción: shortName
ajustada a cheeses
.
... lines 1 - 7 | |
/** | |
* @ApiResource( | |
... lines 10 - 11 | |
* shortName="cheeses" | |
... lines 13 - 14 | |
*/ | |
... lines 16 - 117 |
Ahora ejecuta de nuevo debug:router
:
php bin/console debug:router
¡Eh! /api/cheeses
! ¡Mucho mejor! Y ahora vemos lo mismo en nuestros documentos de la API.
Vale: así podemos controlar qué operaciones queremos en un recurso. Y más adelante aprenderemos a añadir operaciones personalizadas. Pero también podemos controlar bastante sobre las operaciones individuales.
Sabemos que cada operación genera una ruta, y la Plataforma API te da un control total sobre el aspecto de esa ruta. Compruébalo: divide itemOperations
en varias líneas. Entonces, en lugar de decir simplemente "get"
, podemos decir "get"={}
y pasar esta configuración extra.
Prueba a poner "path"=
en, no sé, "/i❤️️cheeses/{id}"
.
... lines 1 - 7 | |
/** | |
* @ApiResource( | |
... line 10 | |
* itemOperations={ | |
* "get"={"path"="/i❤️cheeses/{id}"}, | |
* "put" | |
* }, | |
... line 15 | |
* ) | |
... line 17 | |
*/ | |
... lines 19 - 120 |
Ve a ver los documentos ¡Ja! ¡Eso funciona! ¿Qué más puedes poner aquí? Para empezar, cualquier cosa que se pueda definir en una ruta, se puede añadir aquí - comomethod
, hosts
, etc.
¿Qué más? Bueno, a lo largo del camino, aprenderemos sobre otras cosas específicas de la plataforma API que puedes poner aquí, como access_control
para la seguridad y formas de controlar el proceso de serialización.
De hecho, ¡vamos a aprender sobre ese proceso ahora mismo! ¿Cómo transforma la Plataforma API nuestro objeto CheeseListing
-con todas estas propiedades privadas- en el JSON que hemos estado viendo? Y cuando creamos un nuevo CheeseListing
, ¿cómo convierte nuestro JSON de entrada en un objeto CheeseListing
?
Entender el proceso de serialización puede ser la pieza más importante para desbloquear la Plataforma API.
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 :)
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!
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?
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-platform/core/issues/4344#issuecomment-873418011
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!
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"
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!
defautl utf8 has been deprecated, if you use sf >= 4, setting the option is required:{"path"="/i💚cheeses/{id}", "options"={"utf8":true}}
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!
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!
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!
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.
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
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!
Hi
Yes , it's "very-different world" :p , it's clear now and I will try to apply your advices.
Thank you
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!
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?
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!
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!
// 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
}
}
Hello,
If you are using Symfony 5 with PHP 8, you can use attributes :
`
#[ApiResource(
)]
`