Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

OpenAPI Specification

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.

Confession time: this tutorial is about a lot more than just API Platform. The world of APIs has undergone massive changes over the past few years, introducing new hypermedia formats, standards, specifications, performance tools and more! API Platform lives right in the middle of these: bringing bleeding-edge best practices right into your app. If you truly want to master API Platform, you need to understand modern API development.

I told you earlier that what we're looking at is called Swagger. Swagger is basically an API documentation interface - a sort of, interactive README. Google for Swagger and open their site. Under tools, the one we're using is called Swagger UI.

Yep!

Swagger UI allows anyone to visualize and interact with your API's resources without having any of the implementation in place.

Literally, you could first describe your API - what endpoints it will have, what it will return, what fields to expect - and then use Swagger UI to visualize your future API, before writing even one line of code for it.

Let me show you what I mean: they have a live demo that looks very similar to our API docs. See that swagger.json URL on top? Copy that, open a new tab, and paste. Woh! It's a huge JSON file that describes the API! This is how Swagger UI works: it reads this JSON file and builds a visual, interactive interface for it. Heck, this API might not even exist! As long as you have this JSON description file, you can use Swagger UI.

The JSON file contains all your paths, a description of what each does, the parameters of the input, what output to expect, details related to security... it basically tries to completely describe your API.

So if you have one of these JSON configuration files, you can plug it into Swagger UI and... boom! You get a rich, descriptive interface.

Hello OpenAPI

The format of this file is called OpenAPI. So, Swagger UI is the interface and it understands this sort of, official spec format for describing APIs called OpenAPI. To make things a bit more confusing, the OpenAPI spec used to be called Swagger. Starting with OpenAPI 3.0, it's called OpenAPI and Swagger is just the interface.

Phew!

Anyways, this is all really cool... but creating an API is already enough work, without needing to try to build and maintain this gigantic JSON document on the side. Which is why API Platform does it for you.

Remember: API Platform's philosophy is this: create some resources, tweak any configuration you need - we haven't done that, but will soon - and let API Platform expose those resources as an API. It does that, but to be an extra good friend, it also creates an OpenAPI specification. Check it out: go to /api/docs.json.

Hello giant OpenAPI spec document! Notice it says swagger: "2.0". OpenAPI version 3 is still pretty new, so API Platform 2 still uses the old format. Add ?spec_version=3 to the URL to see... yep! This is that same document in the latest format version.

Now, go back to our API doc homepage and view the HTML source. Ha! The OpenAPI JSON data is already being included on this page via a little swagger-data script tag! That is how this page is working!

To generate Swagger UI from OpenAPI version 3, you can add the same ?spec_version=3 to the URL. Yep, you can see the OAS3 tag. That doesn't change a lot on the frontend, but there are a few new pieces of information that Swagger can now use thanks to the new spec version.

What else Can OpenAPI Do? Code Generation!

But... other than the fact that it gives us this nice Swagger UI, why should we care that there's some giant OpenAPI JSON spec being created behind the scenes? Back on the Swagger site, one of the other tools is called Swagger CodeGen: a tool for creating an SDK for your API in almost any language! Think about it: if your API is fully-documented in a machine-understandable language, shouldn't we be able to generate a JavaScript or PHP library that's customized for talking with your API? You totally can!

The last thing I want to point out is that, in addition to the endpoints, or "paths", the OpenAPI specification also has information about "models". In the JSON spec, scroll all the way to the bottom: it describes our CheeseListing model and the fields to expect when sending and receiving this model. You can see this same info in Swagger.

And woh! It somehow already knows that the id is an integer and that it's readonly. It also knows price is an integer and createdAt is a string in a datetime format. That's awesome! API Platform reads that information directly from our code, which means that our API docs stay up-to-date without us needing to think about it. We'll learn more about how that works along the way.

But before we get there, we need to talk about one other super important thing that we're already seeing: the JSON-LD and Hydra format that's being returned by our API responses.

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
Kiuega Avatar Kiuega | weaverryan | posted 2 years ago | edited

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 <blockquote>'Idle timeout reached for "</blockquote><b>http://127.0.0.1:8000/api/cheeses.json&lt;/b&gt;&quot;.&#039;.

Still, if I try to reach a URL like <b>https://api.github.com/repos/guzzle/guzzle&lt;/b&gt;, 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 : <b>symfony serve --allow-http</b>, so I tested with <b>http</b> instead of <b>https</b>, 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 <b>nelmio_cors</b> config :<b>allow_origin: ['*']</b>. 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
Kiuega Avatar Kiuega | weaverryan | posted 2 years ago | edited

Hello ! And no worries about the delay! :)

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

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

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

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

<blockquote>>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&lt;/blockquote&gt;

It's exactly that. I created a new route to <b>https://127.0.0.1:8000/test&lt;/b&gt;.
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 '<b>request</b>' method below

<u>Example</u> :

<
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();
    }
}

<blockquote>>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. </blockquote>

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.

<u>On the other hand, I also tested something else:</u>

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
Kiuega Avatar Kiuega | weaverryan | posted 2 years ago | edited

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!

<blockquote>>(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.</blockquote>

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

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

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 : <b>Warning: Undefined variable $_SESSION</b>

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
Kiuega Avatar Kiuega | weaverryan | posted 2 years ago | edited

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?

<b>EDIT</b> : 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 2 years ago | 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
Kiuega Avatar Kiuega | weaverryan | posted 2 years ago | edited

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/bastien70/api_platform_tuto

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/bastien70/api_platform_tuto/tree/main/src/Library

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 2 years ago | edited

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:
<br />$cheeseApi->getUsers();<br />$cheeseApi->getCheeses();<br />

instead of:
<br />$userApi->getUsers();<br />$cheeseListingsApi->getCheeses();<br />

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/php-github-api

If you want to fetch commit resources - https://github.com/KnpLabs/php-github-api/blob/2.x/doc/commits.md vs gist resources https://github.com/KnpLabs/php-github-api/blob/2.x/doc/gists.md - 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!

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
    }
}
userVoice