Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
10 Comments
That's because your api_platform.yaml doesn't contain format for json
api_platform:
title: Hello API Platform
version: 1.0.0
formats:
jsonld: ['application/ld+json']
json: ['application/json']
you should add accept method
json: ['application/json']
Hey @Nizar
If you remove the .json
part does it work? I believe the main docs route does not support file extensions (but the API endpoints do)
Cheers!
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
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!
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
Hi,
Why api platform now gives duplicate properties from hydra docs?
For example, my expected JSON POST input looks like this:
{
"first_name": "string",
"last_name": "string",
"email": "string",
"created_at": "2024-05-04T13:57:39.582Z",
"firstName": "string",
"lastName": "string",
"createdAt": "2024-05-04T13:57:39.582Z"
}
instead of this:
{
"first_name": "string",
"last_name": "string",
"email": "string",
"created_at": "2024-05-04T13:57:39.582Z"
}
I've tried setting up symfony and api platform over and over again, generating the entity via maker bundle, the same things happen.
Thanks in advance!
Hey @Zhivko
That's odd. If you look closely the repeated fields are in snake and camel case. I wonder if you may have both fields in your API resource, or for some reason, ApiPlatform is using both conventions to serialize your resources. I'd guess this is a misconfiguration of ApiPlatform
Cheers!
Hi @MolloKhan ,
Thanks for the answer.
Still not sure how to configure API Platform to avoid this duplication?
Here is my ApiResource enabled Symfony Entity class:
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ApiResource]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
/**
* This is the first name of a user
*/
#[ORM\Column(length: 50, nullable: true)]
private ?string $first_name = null;
#[ORM\Column(length: 50, nullable: true)]
private ?string $last_name = null;
#[ORM\Column(length: 255)]
private ?string $email = null;
#[ORM\Column]
private ?\DateTimeImmutable $created_at = null;
public function getId(): ?int
{
return $this->id;
}
public function getFirstName(): ?string
{
return $this->first_name;
}
public function setFirstName(?string $first_name): static
{
$this->first_name = $first_name;
return $this;
}
public function getLastName(): ?string
{
return $this->last_name;
}
public function setLastName(?string $last_name): static
{
$this->last_name = $last_name;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->created_at;
}
public function setCreatedAt(\DateTimeImmutable $created_at): static
{
$this->created_at = $created_at;
return $this;
}
}
As you can see, nothing special, normally created Entity via the Maker bundle.
To me, it seems like API Platform serializes the get methods as well, but why?
Regards,
Zhivko
That's interesting. It could be a bug on ApiPlatform but I'm not sure. I believe the problem happens because you're mixing naming conventions (snake case and camel case). I think the easiest solution would be to stick to one convention, or you can use serialization groups to choose what fields to process
More info here: https://api-platform.com/docs/core/serialization/
Cheers!
"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
}
}
Hi
when I type the following url: https://localhost:8000/api/docs.json in my browser I get the following error:
"title": "An error occurred",
"detail": "Format \"json\" is not supported",
could you help me please ?