Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Swagger UI: Interactive Docs

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.

The amazing interactive documentation that we've stumbled across is not something from API platform! Nope, it's actually an open-source API documentation library called Swagger UI. And the really cool thing about Swagger UI is that, if someone create a file that describes any API, then that API can get all of this for free! I love free stuff! We get Swagger UI because API platform provides that description file out of the box. But more on that in a minute.

Playing with our New API

Let's play around with this. Use the POST endpoint to create a new DragonTreasure. We've recently plundered some "Gold coins"... which we got from "Scrooge McDuck". He's mad. For our purposes, none of the other fields really matter. Down here, hit " Execute" and... boom! When you scroll down, you can see that this made a POST request to /api/dragon_treasures and sent all of that data as JSON! Then, our API returned a "201" status code. A 201 status means that the request was successful and a resource was created. Then it returned this JSON, which includes an id of 1. So, as I said, this isn't just documentation: we really do have a working API! There are a few extra fields here too: @context, @id, and @type We'll talk about those soon.

Now that we have a DragonTreasure to work with, open up this "GET" endpoint, click "Try it Out", then "Execute". Oh, I love it. Swagger just made a GET request to /api/dragon_treasures - this ?page=1 is optional. Our API returned information inside something called hydra:member, which isn't particularly important yet. What matters is that our API did return a list of all of the DragonTreasures we currently have, which is just this one.

So in just a few minutes of work, we have a fully featured API for our Doctrine entity. That is cool.

Content Negotiation

Copy the URL to the API endpoint, open a new tab, and paste that in. Whoa! This... returned HTML? But a second ago, Swagger said that it made a GET request to that URL... and it returned JSON. What's going on?

One feature of API Platform is called "Content Negotiation". It means that our API can return the same resource - like DragonTreasure - in multiple formats, like JSON, or HTML... or even things like CSV. Oh, an ASCII format would be awesome. Anyways, we tell API Platform which format we want by passing an Accept header in the request. When we use the interactive docs, it passes this Accept header for us set to application/ld+json. We'll talk about the ld+json part soon... but, thanks to this, our API returns JSON!

And even though we don't see it here, when you go to a page in your browser, your browser automatically sends an Accept header that says we want text/html. So, this is API Platform showing us the "HTML representation" of our dragon treasures..., which is just the documentation. Watch: when I open the endpoint this URL is for, it automatically executed it.

The point is: if we want to see the JSON representation of our dragon treasures, we need to pass this Accept header... which is super easy, for example, if you're writing JavaScript.

But passing a custom Accept header isn't so easy in a browser... and it would be nice to be able to see the JSON version of this. Fortunately, API Platform gives us a way to cheat. Remove the ?page=1 to simplify things. Then, at the end of any endpoint, you can add . followed by the extension of the format you want: like .jsonld.

Now we see the DragonTreasure resource in that format. API Platform also supports normal JSON out of the box, so we can see the same thing, but in pure, standard JSON.

Where do the new Routes Come From?

The fact that all of this works means that... we apparently have a new route for /api as well as a bunch of other new routes for each operation - like GET /api/dragon_treasures. But... where did these come from? How are they being dynamically added to our app?

To answer that, spin over to your terminal and run:

./bin/console debug:router

I'll make this a bit smaller so we can see everything. Yup! Each endpoint is represented by a normal, traditional route. How are these being added? When we installed API Platform, its recipe added a config/routes/api_platform.yaml file.

api_platform:
resource: .
type: api_platform
prefix: /api

This is actually a route import. It looks a little weird, but it activates API Platform when the routing system is loading. API Platform then finds all of the API resources in our app and generates a route for every endpoint.

The point is that all we need to focus on is creating these beautiful PHP classes and decorating them with ApiResource. API Platform takes care of all the heavy lifting of hooking up those endpoints. Of course, we'll need to tweak the configuration and talk about more advanced things, but hey! That's the point of this tutorial. And we're already off to an epic start.

Next: I want to talk about the secret behind how this Swagger UI documentation is generated. It's called OpenAPI.

Leave a comment!

3
Login or Register to join the conversation

Hi Ryan,

I have an issue. I'm using mysql instead of postgresql (created a docker standard mysql 8 container from mysql:latest).
I was able to create the entity with the maker bundle, the migration, create the database (./bin/console doctrine:database:create) and execute the migration using CLI commands (./bin/console doctrine:migrations:migrate). The database was created and the tables as well.
All steps went successfully until I tried to submit a post request from the https://127.0.0.1:8000/api. I get the following error:

An exception occurred in the driver: SQLSTATE[HY000] [1049] Unknown database 'root'

The problem is that for some reason it is trying to query a database named root, when in fact the database that was earlier created is called app.

Created database app for connection named default
was the message I got after executing ./bin/console doctrine:database:create

In my .env file I have:

DATABASE_URL="mysql://root:testpassword@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"

Any thoughts on this one?

Thanks,
Radu

Reply

Hey Radu!

Hmm. Do you have a docker-compose.yml file? If you do and you have Docker running AND you have a container called database, then when you access the site, the symfony binary is overriding the DATABASE_URL env var and setting it to point at that Docker container. If you're setting up your database locally (without Docker), just don't bother starting Docker and delete the docker-compose.yml file entirely (or at least the matching service).

If my guess is incorrect, let me know!

Cheers!

1 Reply

Hey @weaverryan,

Thanks for the quick reply! You were indeed correct! I've recreated my local container after renaming the database service in the docker-compose.yml file to my_app_database and the issue went away! :)

Cheers,
Radu

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice