Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Especificación OpenAPI

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Hora de la confesión: este tutorial trata de mucho más que de la Plataforma API. El mundo de las APIs ha experimentado grandes cambios en los últimos años, introduciendo nuevos formatos hipermedia, estándares, especificaciones, herramientas de rendimiento, etc. La Plataforma API se encuentra justo en medio de todo esto: aportando las mejores prácticas de vanguardia a tu aplicación. Si realmente quieres dominar la Plataforma API, tienes que entender el desarrollo moderno de las API.

Ya te he dicho que lo que estamos viendo se llama Swagger. Swagger es básicamente una interfaz de documentación de la API, una especie de README interactivo. Busca Swagger en Google y abre su sitio. En la sección de herramientas, la que estamos utilizando se llama Swagger UI.

¡Sí!

Swagger UI permite a cualquiera visualizar e interactuar con los recursos de tu API sin tener ninguna implementación en marcha.

Literalmente, podrías describir primero tu API -qué rutas tendrá, qué devolverá, qué campos esperar- y luego utilizar Swagger UI para visualizar tu futura API, antes de escribir ni siquiera una línea de código para ella.

Deja que te muestre lo que quiero decir: tienen una demostración en vivo que se parece mucho a nuestros documentos de la API. ¿Ves esa URL de swagger.json en la parte superior? Cópiala, abre una nueva pestaña y pégala. ¡Woh! ¡Es un enorme archivo JSON que describe la API! Así es como funciona Swagger UI: lee este archivo JSON y construye una interfaz visual e interactiva para él. Diablos, ¡es posible que esta API ni siquiera exista! Siempre que tengas este archivo de descripción JSON, puedes utilizar Swagger UI.

El archivo JSON contiene todas tus rutas, una descripción de lo que hace cada una, los parámetros de la entrada, qué salida esperar, detalles relacionados con la seguridad... básicamente trata de describir completamente tu API.

Así que si tienes uno de estos archivos de configuración JSON, puedes conectarlo a la interfaz Swagger y... ¡boom! Obtienes una interfaz rica y descriptiva.

Hola OpenAPI

El formato de este archivo se llama OpenAPI. Así pues, Swagger UI es la interfaz y entiende esta especie de formato de especificación oficial para describir APIs llamado OpenAPI. Para hacer las cosas un poco más confusas, la especificación OpenAPI solía llamarse Swagger. A partir de OpenAPI 3.0, se llama OpenAPI y Swagger es sólo la interfaz.

¡Uf!

De todos modos, todo esto está muy bien... pero crear una API ya es suficiente trabajo, sin necesidad de intentar construir y mantener este gigantesco documento JSON al margen. Por eso la Plataforma API lo hace por ti.

Recuerda: La filosofía de la Plataforma API es la siguiente: crea algunos recursos, modifica cualquier configuración que necesites -no lo hemos hecho, pero lo haremos pronto- y deja que la Plataforma API exponga esos recursos como una API. Eso es lo que hace, pero para ser un buen amigo más, también crea una especificación OpenAPI. Compruébalo: ve a /api/docs.json.

¡Hola documento gigante de la especificación OpenAPI! Fíjate en que dice swagger: "2.0". La versión 3 de OpenAPI es todavía bastante nueva, así que la Plataforma API 2 sigue utilizando el formato antiguo. Añade?spec_version=3 a la URL para ver... ¡sí! Este es el mismo documento en la última versión del formato.

Ahora, vuelve a nuestra página de inicio del documento de la API y ve el código fuente HTML. ¡Ja! ¡Los datos JSON de OpenAPI ya se están incluyendo en esta página a través de una pequeña etiqueta de script swagger-data! ¡Así es como funciona esta página!

Para generar la Swagger UI de la versión 3 de OpenAPI, puedes añadir el mismo ?spec_version=3a la URL. Sí, puedes ver la etiqueta OAS3. Esto no cambia mucho en el frontend, pero hay unos cuantos datos nuevos que Swagger puede utilizar ahora gracias a la nueva versión de las especificaciones.

¿Qué más puede hacer OpenAPI? ¡Generar código!

Pero... aparte del hecho de que nos proporciona esta bonita interfaz de Swagger, ¿por qué debería importarnos que se cree una gigantesca especificación JSON de OpenAPI entre bastidores? Volviendo al sitio de Swagger, una de las otras herramientas se llama Swagger CodeGen: ¡una herramienta para crear un SDK para tu API en casi cualquier lenguaje! Piénsalo: si tu API está completamente documentada en un lenguaje comprensible para la máquina, ¿no deberíamos poder generar una biblioteca JavaScript o PHP personalizada para hablar con tu API? ¡Se puede perfectamente!

Lo último que quiero señalar es que, además de los puntos finales, o "rutas", la especificación OpenAPI también tiene información sobre los "modelos". En la especificación JSON, desplázate hasta el final: describe nuestro modelo CheeseListing y los campos que hay que esperar al enviar y recibir este modelo. Puedes ver esta misma información en Swagger.

Y ¡oh! De alguna manera ya sabe que el id es un integer y que esreadonly. También sabe que el precio es un integer y que createdAt es una cadena con formatodatetime. ¡Eso es increíble! La Plataforma API lee esa información directamente de nuestro código, lo que significa que nuestros documentos de la API se mantienen actualizados sin que tengamos que pensar en ello. Aprenderemos más sobre cómo funciona esto a lo largo del camino.

Pero antes de llegar ahí, tenemos que hablar de otra cosa súper importante que ya estamos viendo: el formato JSON-LD e Hydra que devuelven las respuestas de nuestra API.

Leave a comment!

23
Login or Register to join the conversation

Hello! Thanks for the tutorials!

I have this problem: after submit my form (which building by Angular):
The type of the "myDataName" attribute must be "bool", "string" given.

Have you any idea if I can force the data?

Reply

Thanks! bug fixed by using the following code:

/**
* @ApiResource(
* denormalizationContext={
* "disable_type_enforcement"=true
* }
* )
*/

1 Reply

Thanks for sharing your solution to others. Cheers!

Reply
Kiuega Avatar

Wait ... I didn't know that we could generate a PHP library to use our API more easily in the future! It's awesome !

Is this the model that Stripe followed to create its different libraries (PHP, Ruby, Node etc)? I use the Stripe-php library and find it so useful! So that means we could do the exact same thing with this build tool?

Or would it be better to do something by hand to get something better?

I'm just discovering API Platform and it's great! Thank you !

Reply

Hey Kiuega !

> Is this the model that Stripe followed to create its different libraries (PHP, Ruby, Node etc)? I use the Stripe-php library and find it so useful! So that means we could do the exact same thing with this build tool?

I believe so, yes! But, in practice, it might be using different tools ands specs internally. What I mean is, internally, Stripe may have their own OpenAPI-like system for "describing" their API and their own tools for generating code based on this. But the philosophy is the same: describe your API in some language, then use tools to generate SDK's from this.

> Or would it be better to do something by hand to get something better?

If you're building an API that will need SDK's in multiple languages, using this is probably the right idea. If you didn't like the code it generated in PHP, it would probably be worth creating a custom "PHP generator" for open API to generate the code you want. But then, as/if your API changes, you could re-generate new SDK's for all languages.

If you were building an API for your *own* use, then you could still use this to make your life easier :). Or, you could use it as a starting point, then modify/customize the generated code (the negative of this being that, of course, if you change your API, you won't be able to regenerate stuff).

The tl;dr is: yes, this is the way to do it :). But if you want to customize the generated code to be just the way you like it, you might need to do more work.

Cheers!

1 Reply
Kiuega Avatar

Hello ! So I'm trying to create my own bookstore for our training cheeses! Without going through a generator, but rather doing everything by hand.

Everything is in place, but I cannot make a request on the API in PHP. I'm still getting the error

'Idle timeout reached for "

http://127.0.0.1:8000/api/cheeses.json".'.

Still, if I try to reach a URL like https://api.github.com/repo..., it works and I got my answer fine.

How come it doesn't work for our API?

Here is how I did it:


use Symfony\Contracts\HttpClient\HttpClientInterface;


public function __construct(private HttpClientInterface $client){}


protected function request(string $method, string $path, array $params = [])
{
//it returns this error : 'Idle timeout reached for "http://127.0.0.1:8000/api/cheeses.json".'

$response = $this->client->request('GET', 'https://127.0.0.1:8000/api/cheeses.json', [
'headers' => [
'Content-Type' => 'application/json',
]
]);

dd(json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR));



//works great

$response = $this->client->request('GET', 'https://api.github.com/repos/guzzle/guzzle', [
'headers' => [
'Content-Type' => 'application/json',
]
]);

dd(json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR));

}

When I'm starting the server, I launch : symfony serve --allow-http, so I tested with http instead of https, same thing.

However, I can access to the url in my browser. And I tested to send a request to the URL with Postman and it works! It's just with PHP that it doesn't work.

I also added in my nelmio_cors config :allow_origin: ['*']. But same.


nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['*']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/': null

EDIT : Ok after several tries, I think I know where the problem is coming from, but I don't know how to fix it. In fact if I have the API on a symfony application on the one hand (https://127.0.0.1:8000), and another symfony application with my library on the other hand (https://127.0.0.1: 8001). The two are on a different port, and there it works, I can access the API with the HttpClient.

On the other hand if the API is on the same domain / port as the place from which I am calling the API, then it loops and it fails to reach it. It's very boring

Reply

Hey Kiuega!

Sorry again for the slow reply - I've been particularly buried lately - but almost on the other side now :).

Ok, this is interesting. I have a few things to check/try:

1) What happens if you, from the command line, just curl http://127.0.0.1:8000/api/cheeses.json. Does it work there?

2) Are you using the Symfony web server or the built-in PHP web server? Or something else? And, where are you trying to make the request from? What I mean is, did you, for example, create a controller in your app so that when you go to http://127.0.0.1:8000/test it executes the code that makes a request to http://127.0.0.1:8000/api/cheeses.json ? If so, depending in your web server... or even session-related stuff, your first request might be blocking your second request. Or, to say it differently, your second request (to the API) may be waiting for your first request to finish before it executes. That never happens... so it eventually times out.

Let me know if any of this helps!

Cheers!

Reply
Kiuega Avatar

Hello ! And no worries about the delay! :)

>What happens if you, from the command line, just curl http://127.0.0.1:8000/api/cheeses.json. Does it work there?

If I run this command from my terminal, then it works, I get the results!

>Are you using the Symfony web server or the built-in PHP web server?

Yes i'm using the Symfony web server => symfony serve --allow-http

>And, where are you trying to make the request from? What I mean is, did you, for example, create a controller in your app so that when you go to http://127.0.0.1:8000/test it executes the code that makes a request to http://127.0.0.1:8000/api/cheeses.json

It's exactly that. I created a new route to https://127.0.0.1:8000/test.
Then in the action of this route, I call the service that I created which allows to communicate with the API, which, in the end, will call the 'request' method below

Example :

<
class AbstractService
{
public function __construct(private HttpClientInterface $client){}

/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
*/
protected function request(string $method, string $path, array $params = [])
{
$defaultOptions = [
'headers' => [
'Content-Type' => 'application/ld+json',
'Accept' => 'application/ld+json',
],
];

$response = $this->client->request(
$method,
'https://localhost:8001/api/cheeses',
array_merge($defaultOptions, $params)
);

return $response->toArray();
}
}

>your first request might be blocking your second request. Or, to say it differently, your second request (to the API) may be waiting for your first request to finish before it executes.

That's what I told myself too. So I created a command to call the API without having to go through the controller, and the error message is the same.

On the other hand, I also tested something else:

If I call the API from another app then it works.

Since the two applications are not on the same port, then it works, and presumably, it confirms what you said previously.

But it's very annoying because I would really like to be able to use my API from the same application I developed it in.

Example: I am creating a project that will have a web application and an application made with Flutter. So I'm creating an API to make things easier. But in the web app, I would still like to go through this same API to fetch or post data rather than going directly through the database like we would without an API (which I think is the best thing to do). to keep some consistency).

So if I have a route where I need to display the list of cheeses, I have to be able to retrieve it from the API. But like you said, maybe the request is blocked by the 1st. In this case, how to proceed?

Do I have to create a second web application that will communicate with the API of the 1st?

Reply

Hey Kiuega !

Ok, so I think we're on the right track here :). Though, there is one thing that I absolutely cannot understand, and it makes me wonder if I'm missing something:

> So I created a command to call the API without having to go through the controller, and the error message is the same.

That doesn't make sense to me :). Let's ignore that for a moment and back up and look at *why* the request might be blocked when trying to call it from a controller. There are 2 reasons that I know of:

1) Some dev web servers can literally only handle 1 request at a time. So if you make a request from inside a request... the 2nd waits for the first to finish.

BUUT, I've just verified that this is NOT the case for the "symfony" binary web server: I was able to make a request from inside the controller to the same app without any issues.

2) Session locking. You can read about this here - https://stackoverflow.com/q... - including a workaround to try to see if it fixes anything.

Buuuuut, neither of these explains the command situation you described above. I simply can't imagine how running a command would cause a web request to fail!

Let me know if (2) sheds any light... but I think that we're missing something... and I don't know what it is.

Cheers!

Reply
Kiuega Avatar

Hey Ryan!

So there is (a little bit) new!

For the command, it was a mistake on my part, now it works!

Which seems to confirm your theory about blocked requests!

>(1)BUUT, I've just verified that this is NOT the case for the "symfony" binary web server: I was able to make a request from inside the controller to the same app without any issues.

Yes it's very weird... I confirm that I run the following command : symfony serve

>(2) Session locking. You can read about this here - https://stackoverflow.com/q... - including a workaround to try to see if it fixes anything.

So I'm not sure I understood how I should implement this.

On the one hand, to make things easier, I'm now doing a (hard-written) request directly from my controller like this:

(Port 8001 is the correct port in case you were wondering, it's just that I have another thing open at the same time, but it was the same under port 8000 when it was available)


/**
* @Route("/test", name="test_page")
*/
public function test(HttpClientInterface $client)
{
$response = $client->request('GET', 'https://127.0.0.1:8001/api/cheeses', [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]
]);

dd($response->toArray());
return $this->redirectToRoute('homepage');
}

So if I'm the stackoverflow help topic, should I save the session just before my API call with the HttpClient? So like this?


/**
* @Route("/test", name="test_page")
*/
public function test(HttpClientInterface $client, Request $request)
{
$session = $request->getSession();
$session->save();

$response = $client->request('GET', 'https://127.0.0.1:8001/api/cheeses', [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]
]);

dd($response->toArray());
return $this->redirectToRoute('homepage');
}

I'm not sure I'm doing it right, since it throws me this error : Warning: Undefined variable $_SESSION

Thank you very much for your time! <3

Reply

Hey Kiuega!

> So if I'm the stackoverflow help topic, should I save the session just before my API call with the HttpClient? So like this?

Hmm yea, that's exactly what I would expect. The undefined variable is super odd, and makes me think that the session is not the problem (my guess is that we're, sort of, short-circuiting the session saving... but the session wasn't actually started yet. But that's a guess). There may be another way to test to see if session locking is the problem:


# config/packages/framework.yaml
framework:
session:
enabled: false

Then you won't need the extra code in your controller. This should disable the session entirely. It's an experiment to see if it makes any difference. If it does NOT (which, I kind of have a feeling that it won't at this point), then I'm not sure what the problem is! It's possible that, under certain situations, the "symfony server" runs something that only handles 1 request at a time and in other situations (like mine) it can handle multiple. But, again, that's a guess. If I run symfony server:status, I see output like this:

> The Web server is using PHP FPM 8.0.3
> Workers
> PID 34093: /usr/local/Cellar/php/8.0.3/sbin/php-fpm --nodaemonize --fpm-config /Users/weaverryan/.symfony/php/0085bc6ff945cb8cebc11c980822d9d72cc72af2/fpm-8.0.3.ini --force-stderr

I'm not sure, but it's possible that, sometimes, it runs without php-fpm... and in that case, it may only handle one process/request.

Cheers!

1 Reply
Kiuega Avatar

Hello! You were right about the session, it doesn't work either.

Regarding the symfony server:status

I actually have something different :


Local Web Server
Listening on https://127.0.0.1:8001
The Web server is using PHP CLI 8.0.7

Local Domains

Workers
PID 275864: /usr/bin/php8.0 -S 127.0.0.1:45809 -d variables_order=EGPCS /home/bastien/.symfony/php/d37513e82a730005aefcd831b745167073ae20ac-router.php

Environment Variables
None

Would there be a way to tell it to use php-fpm?

EDIT : I dit it! (thanks to you ! You're the best, come to France whenever you want and I'll give you tons of cheese, a French baguette, and a beret!). I had to use PHP FPM as you mentioned! So I installed it, activated it, and that's it, everything works perfectly.

Ryan, good job! High five!
Thank you very much ! :D

I have one very last question to clarify if I want to continue in this direction. Some routes require authentication.

If I authenticate with the system we have set up, then I POST on https://127.0.0.1:8000/api/cheeses to create a cheese, I thought it would work, but it still gives me an error telling me that authentication is required.

Should I insert something special into the headers of each request with the HttpClientInterface, like getting the current user's token (with the TokenStorageInterface class) and injecting it into the request if the token is different from null?

I find it a bit hacky, so I wonder if there isn't something more professional?

Reply

Hey Kiuega!

Woo hoo! That was a fun thing to debug... and before this, I was only guessing that this is how the Symfony binary would work (falling back to "php" if fpm is not available). Now I know for sure!

> come to France whenever you want and I'll give you tons of cheese, a French baguette, and a beret!

I would love that! Leanna would love it even more (and she's been practicing her French!).

Cheers!

1 Reply
Fabrice Avatar

I have one very last question to clarify if I want to continue in this direction. Some routes require authentication.

If I authenticate with the system we have set up, then I POST on https://127.0.0.1:8000/api/cheeses to create a cheese, I thought it would work, but it still gives me an error telling me that authentication is required.

Should I insert something special into the headers of each request with the HttpClientInterface, like getting the current user's token (with the TokenStorageInterface class) and injecting it into the request if the token is different from null?

I find it a bit hacky, so I wonder if there isn't something more professional?

Reply

Hey Fabrice!

Indeed - when you make that request from inside your app, that is a "machine to machine" request - it wont be sharing the session cookie information. Usually machine to machine authentication uses a different type of authentication... like some sort of API token. think you could do what you're saying (you would actually need to read the session cookie value from the request, and put this cookie into the HttpClientInterface request), but you're correct that it's a bit odd ;). It might be less hacky if you added some extra way for a machine to authenticate to your API - like an API token. If it's your own server talking to your own server... then this could, for simplicity, even be a single, secret key that both apps know about (nothing fancier needed). In that case, you would have a custom authenticator that looks for this token on a header and authenticates if it's present. If you want to authenticate "as a specific user", then... hmm... perhaps you could specify the user id as a header and read that from there. I IS all a bit weird... but maybe not AS weird as it sounds at first. It's common for one machine to pass an API token to another machine that authenticates them as a specific user (there may be a database of tokens that associates that token with a specific user). In this case, since you own everything yourself, you're, sort of, short-circuiting things by having a single API token secret and allowing the user id to be passed as a header. The down side is security - if someone ever got that secret key, they could authenticate as any user. If that's a concern, you could generate JWT tokens that contain the user id and use pass that back and forth (in that scenario, only the app MAKING the API request would need the super secret key... but as I think you just have 1 big app, that might not make any difference).

Ok, I'll end my rambling - I hope this gives you some direction.

Cheers!

Reply
Kiuega Avatar
Kiuega Avatar Kiuega | weaverryan | posted hace 1 año | edited

Hello @wweaverryan !

Okay thank you for this information!

So I set up a JWT authentication. I configured it to put the token in a cookie, and I get the cookie from the service making the request, and put it in the header ... and it works!

Little by little, my very simplified SDK is taking shape. By the way, you've helped me tremendously, so I'll post it here when I have something really concrete to present. You can add a note if you wish to show others how to call the API with this kind of custom SDK :)

Everything looks OK! I think it's 95% finished. There is one more thing to clear up in this regard before I publish the github repository for everyone.

If an application using the API wants to get the User object connected, what means then should I use?

I mean ... now that we are using JWT authentication, the user is no longer 'officially' logged in (in the profiler), so I can't check their roles, or call voters to restrict access .

Therefore, what would be the best solution between:

1. Create in the API an endpoint 'https://127.0.0.1:8000/users/me' to retrieve the current user (thanks to the JWT token) and therefore returning a User object with information such as roles ( and others).

2. A way to directly decode the JWT token like on jwt.io?

Reply

Hi Kiuega!

> So I set up a JWT authentication. I configured it to put the token in a cookie, and I get the cookie from the service making the request, and put it in the header ... and it works

Yay!

> Little by little, my very simplified SDK is taking shape. By the way, you've helped me tremendously, so I'll post it here when I have something really concrete to present. You can add a note if you wish to show others how to call the API with this kind of custom SDK :)

Double yay! 😀

> If an application using the API wants to get the User object connected, what means then should I use?

Before I answer, I want to clarify something. What do you mean by "application"? Is this still some "server"/code that YOU own? Or will 3rd party applications be able to make API requests on behalf of users on your app? If it is the second one, then THAT is when. you need OAuth (which is just a strategy to safely distribute API tokens to 3rd party apps).

In either situation, you ultimately end up with a token that you are using for authentication. So, let me keep answering ;)

> I mean ... now that we are using JWT authentication, the user is no longer 'officially' logged in (in the profiler), so I can't check their roles, or call voters to restrict access .

Hmm. This does not need to be the case. If you implement JWT with, for example, a custom authenticator, then the user WILL be logged on. How/where have you added the JWT logic?

> 1. Create in the API an endpoint 'https://127.0.0.1:8000/users/me' to retrieve the current user (thanks to the JWT token) and therefore returning a User object with information such as roles ( and others).

The first one isn't technically RESTful, but it's very common. The other common method is just /users/5 - and then you have access to read user "5" only if you are that user. This requires whoever is using your API to know the user's id.

> 2. A way to directly decode the JWT token like on jwt.io?

I'm not sure I follow what you are thinking here.

Cheers!

Reply
Kiuega Avatar

Hello!

>What do you mean by "application"?

I mean an external symfony application (created by someone), which would like to use our API.

Very simple example:

We create an API allowing people to create their restaurant (with categories, products, reservations, orders).

So we store all the data of all the restaurants, and we create an API allowing someone to create their own application with their own design, but in which they will use our API as a database (so they will not have their own own database).

This is a typical example. And in this case, I think I'm not mistaken if I say to use JWT authentication with the https://github.com/lexik/Le... And thanks to an SDK, he will be able to more easily use the API that I will have created. The SDK would take care of putting the client's JWT token in the header.

And that's where my question comes from regarding official authentication (where we see in the profiler that the user is well connected)

And in the event that the person creates his own symfony application (his restaurant therefore but always using our API), but also having his own data, therefore his own database, then he will have to set up an API key which will be attached to the User object, and which will be used to authenticate it for requests. Is that it?

So, in this case, why oAuth?

>I'm not sure I follow what you are thinking here.

In https://jwt.io/ , when you put the token, you can see the decoded token on the right side, with some info related to the user.
So, my question is, is it possible, in a traditionnal Symfony app using our API, to decode in PHP, the current user token, like in jwt.io, to have the user info ?

Thanks !

Reply

Hey kiuega!

> We create an API allowing people to create their restaurant (with categories, products, reservations, orders).

Ah, ok. Based on this, you do not need OAuth because each restaurant will use the API to access THEIR restaurant data. That. is different than, for example, a user using YOUR site directly (and entering their information) and then while that user uses a totally external "Restaurant Foo" site, "Restaurant Foo" wants to access THAT user's data. on your system. The difference is kind of subtle - but it all comes down to the question: "is the machine that's using the API own the information they are trying to access". If yes (which is true in your case), then OAuth is not needed. This is more like using Stripe. If I sign up for Stripe, I can go into their admin area, generate an API token, then start using that in my code. It's that simple.

> And in the event that the person creates his own symfony application (his restaurant therefore but always using our API), but also having his own data, therefore his own database, then he will have to set up an API key which will be attached to the User object, and which will be used to authenticate it for requests. Is that it?

Hmm. It depends on what you mean by User object. When that person generates an API token for your API, it will be attached to that user's *restaurant*. So if you are thinking of each Restaurant as a "User", then definitely (and this IS a valid way to think of things). In that case, in you looked in the web debug toolbar, you would be authenticated as a specific "restaurant". That is the correct way to do things :).

> In https://jwt.io/ , when you put the token, you can see the decoded token on the right side, with some info related to the user.
> So, my question is, is it possible, in a traditionnal Symfony app using our API, to decode in PHP, the current user token, like in jwt.io, to have the user info ?

Sure! But I would argue that there is little value in this :).

When it comes to authentication (so, when a request comes to your API), you need to know two things:

1) WHO (which user or restaurant) is trying to authenticate
2) Some proof that they ARE this user/restaurant.

JWT is just an alternate way of doing these two steps. Let's look at two common approaches: storing a random token (non JWT) in a database vs generating a JWT:

A) JWT: you generate this and don't need to store it in the database (though, you can if you want) because the related user info is IN the token. Thus, when you read this off the header, you can simply decode the JWT to see which restaurant this is for (part 1 above). By verifying the JWT signature, you prove that this is not a faked token (part 2 from above)
B) Random token stored in the database that is attached to the restaurant. In this case, when you read the token off the header, you look for it in the database. If it is THERE, that proves part (2) above (if I invent a token, it won't be found in the database). Then I look at which Restaurant the token is attached to for part (1).

So these are just two ways to solve the same problem - JWT stores the related user in the token itself, while the random token stores that info in the database.

The REAL advantage of JWT - which is a use-case that not many people have - is that you don't need to store the information in the database. If you have a big microservice infrastructure, this means that any microservice can receive a JWT and use it, without making an API request back to some central authentication server to verify if it's correct. You need to be pretty big to have this problem :).

Additionally, even if you DO use JWT, you probably WILL still want to store them in the database... which kind of defeats the purpose. Why? Because that's the only way to (A) list the tokens that are currently active for a restaurant and (B) "revoke" a token before it expires.

Let me know if this clarifies :).

Chees!

Reply
Kiuega Avatar

Hello Ryan!

Yes it already seems a little clearer to me!

So I moved forward a bit, and here is the end result :

https://github.com/bastien7...

This is the entire training on API Platform, for Symfony 5 and PHP 8, to which is added the 'custom SDK' part, which you can see here

https://github.com/bastien7...

I updated the authentication system with LexikJwtBundle.

And now we can use the API in PHP like this:


/**
* @Route("/test", name="test_page")
*/
public function test(ApiClient $client)
{
$response = $client->cheeses->create([
'title' => 'my super cheese',
'price' => 50000,
'description' => 'mmmh very good'
]);

dd($response);
}

Automatically, for each request, I check if there is a bearer token in cookie, in which case, I include it in the header.

I created a service for each resource, trying to build on the same template as Stripe's PHP SDK.

There, I don't know what it's worth, but anyway, it seems to work

Reply

Hey @kiuega!

Nice work! Follow the Stripe SDK, in my opinion, is a great way to go - I've always been blow away by how well they manage things :).

Cheers!

Reply
Daniel W. Avatar
Daniel W. Avatar Daniel W. | posted hace 2 años

I tried to generate an SDK with swagger but every endpoint ends up in its own api. So when I generate an SDK with User and cheese-listing Entities the generated SDK has 2 api objects one for user and one for cheese-listing instead of just one. Is there a way to fix that?
It might make sense in this example but imagine you have 5 or more closly related entities and you need to initialize and configure 5 or more apis for that.
This can't be the right behavior.

example code:
I want to call the api like that:

$cheeseApi->getUsers();
$cheeseApi->getCheeses();

instead of:

$userApi->getUsers();
$cheeseListingsApi->getCheeses();

Reply

Hey Daniel W.!

Hmm. I'm really not sure about this, unfortunately. The way it's designed could be just "the way the author of that particular SDK generator" decided to do things, or it could truly be how it's "supposed" to be generated based on the OpenAPI rules. I don't know enough about it to be sure. However, I *can* say that I've seen SDK's generated exactly like this before. For example, the GitHub API library is much like this - https://github.com/KnpLabs/...

If you want to fetch commit resources - https://github.com/KnpLabs/... vs gist resources https://github.com/KnpLabs/... - you use two different "clients" (sort of):


$commits = $client->api('repo')->commits()->all();

$gists = $client->api('gists')->all('public');

So, there is technically only one client, but everything is sub-divided. Now, even this, I'll admit, is a bit better than what you're seeing, as there is *at least* a centralized client.

Oh, but I can tell you one more thing (after some research) :). If you look at your OpenAPI spec - /api/docs.json - you will see that below each operation, there is a "tags". THAT, apparently, is what determines which "Api" it will belong to. So, I'm not sure if it will make a lot of sense, but you could manually change those all to the same string, and see if you like the generated code better :).

Cheers!

1 Reply
Cat in space

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

Este tutorial funciona muy bien para Symfony 5 y la Plataforma API 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
    }
}