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

Swagger: Documentación de API instantánea e interactiva

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.

Actualmente estamos estudiando algo llamado Swagger: una interfaz de documentación de API de código abierto. Pronto hablaremos más sobre ella, pero la idea es básicamente ésta: si tienes una API -construida en cualquier lenguaje- y creas alguna configuración que describa esa API en un formato que Swagger entienda, ¡boom! Swagger puede mostrarte esta hermosa documentación interactiva. Entre bastidores, la Plataforma API ya está preparando esa configuración para Swagger.

¡Vamos a jugar con ella! Abre la ruta POST. Dice lo que hace y muestra cómo debe ser el JSON para utilizarlo. ¡Muy bien! ¡Haz clic en "Probar"! Veamos lo que hay en mi cocina: un poco de "queso azul a medio comer", que todavía está... probablemente bien para comer. Lo venderemos por 1$. ¡Qué ganga! Y... ¡Ejecutar!

¿Qué ha pasado? Desplázate hacia abajo. ¡Woh! POST ¡Acaba de hacer una petición a/api/cheese_listings y ha enviado nuestro JSON! Nuestra aplicación ha respondido con un código de estado 201 y... unas claves JSON de aspecto extraño: @context @id y @type. Luego tiene los datos normales del nuevo listado de quesos: el autoincremento id, title, etc. ¡Eh! Ya tenemos una API que funciona... ¡y esto acaba de demostrarlo!

Cierra el POST y abre el GET que devuelve una colección de listados de quesos. Prueba éste también: ¡Ejecuta! Sí... ahí está nuestro listado... pero no es JSON crudo. Este material extra se llama JSON-LD. Es simplemente JSON normal, pero con claves especiales -como @context - que tienen un significado específico. Entender JSON-LD es una parte importante para aprovechar la Plataforma API, y pronto hablaremos más de ello.

De todos modos, para hacer las cosas más interesantes, vuelve a la ruta POST y crea una segunda lista de quesos: un bloque gigante de queso cheddar... por 10 $. ¡Ejecuta! Mismo resultado: código de estado 201 e id 2.

Vuelve a probar la ruta de recogida GET. Y... ¡bien! Dos resultados, con los identificadores 1 y 2. Y si queremos obtener sólo un listado de quesos, podemos hacerlo con la otra ruta GET. Como puedes ver, el id del listado de quesos que queremos obtener forma parte de la URL. Esta vez, cuando hacemos clic para probarlo, ¡genial! Nos da una casilla para el identificador. Usa "2" y... ¡Ejecuta!

Esto hace una petición GET muy sencilla a /api/cheese_listings/2, que devuelve un código de estado200 y el conocido formato JSON.

Negociación del tipo de contenido

¡Qué guay es esto! Un "CRUD" completo de la API sin ningún trabajo. Por supuesto, el truco será personalizar esto a nuestras necesidades exactas. Pero, ¡vaya! Es un comienzo increíble.

Intentemos acceder a nuestra API directamente -fuera de Swagger- sólo para asegurarnos de que todo esto no es un truco elaborado. Copia la URL, abre una nueva pestaña, pega y... ¡hola JSON! ¡Woh! Hola... ¿Página de documentación de la API de nuevo?

Nos ha desplazado hasta la documentación de esta ruta y la ha ejecutado con el id 2... lo cual está bien... pero ¿qué está pasando? ¿Tenemos realmente una API que funciona o no?

En la Plataforma API hay algo llamado negociación de tipo de contenido. Convenientemente, cuando ejecutas una operación, Swagger te muestra cómo podrías hacer esa misma petición utilizando curl en la línea de comandos. E incluye una pieza crítica:

-H "accept: application/ld+json"

Que dice: haz una petición con una cabecera Accept establecida en application/ld+json. La petición está dando una pista a la Plataforma API de que debe devolver los datos en este formato JSON-LD. Te des cuenta o no, tu navegador también envía esta cabecera: como text/html... porque... es un navegador. Eso básicamente le dice a la Plataforma API:

¡Eh! Quiero el CheeseListing con id 2 en formato HTML.

La Plataforma API responde haciendo todo lo posible por hacer exactamente eso: devuelve la página HTML Swagger con el id 2 de CheeseListing ya mostrado.

Fingir el Content-Type

Esto no es un problema para un cliente de la API porque establecer una cabecera Accept es fácil. Pero... ¿hay alguna forma de... "probar" la ruta en un navegador? Totalmente, puedes hacer trampa: añade .jsonld al final de la URL.

Y ¡boom! Esta es nuestra ruta de la API en formato JSON-LD. He llamado a esto "trampa" porque este pequeño "truco" de añadir la extensión sólo está pensado para el desarrollo. En el mundo real, deberías establecer la cabecera Accept en su lugar, como si estuvieras haciendo una petición AJAX desde JavaScript.

Y, mira esto: cambia la extensión a .json. ¡Eso parece un poco más familiar!

Este es un gran ejemplo de la filosofía de la Plataforma API: en lugar de pensar en rutas, controladores y respuestas, la Plataforma API quiere que pienses en crear recursos API -como CheeseListing - y luego exponer ese recurso en una variedad de formatos diferentes, como JSON-LD, JSON normal, XML o exponerlo a través de una interfaz GraphQL.

¿De dónde vienen estas rutas?

Por supuesto, por muy increíble que sea, si eres como yo, probablemente estés pensando

Esto es genial... pero ¿cómo se han añadido mágicamente todas estas rutas a mi aplicación?

Después de todo, normalmente no añadimos una anotación a una entidad... ¡y de repente obtenemos un montón de páginas funcionales!

Busca tu terminal y ejecuta:

php bin/console debug:router

¡Genial! La plataforma de la API está trayendo varias rutas nuevas: api_entrypoint es una especie de "página de inicio" de nuestra api, que, por cierto, puede devolverse como HTML -como hemos estado viendo- o como JSON-LD, para un "índice" legible por la máquina de lo que incluye nuestra API. Más adelante hablaremos de ello. También hay una URL /api/docs -que, para el HTML es lo mismo que ir a /api, otra llamada /api/context -más sobre esto en un minuto- y abajo, 5 rutas para los 5 nuevos puntos finales. Cuando añadamos más recursos más adelante, veremos más rutas.

Cuando instalamos el paquete de la Plataforma API, su receta añadió un archivoconfig/routes/api_platform.yaml

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

Así es como la Plataforma API añade mágicamente las rutas. No es muy interesante, pero ¿ves ese type: api_platform? Eso dice básicamente:

¡Oye! Quiero que la Plataforma API sea capaz de añadir dinámicamente las rutas que quiera.

Lo hace encontrando todas las clases marcadas con @ApiResource -sólo una en este momento-, creando 5 nuevas rutas para las 5 operaciones, y prefijando todas las URL con/api. Si quieres que tus URLs de la API vivan en la raíz del dominio, sólo tienes que cambiar esto por prefix: /.

Espero que ya estés emocionado, ¡pero hay mucho más de lo que parece! A continuación, vamos a hablar de la especificación OpenAPI: un formato de descripción de la API estándar en la industria que da a tu API Swagger superpoderes... gratis. Sí, tenemos que hablar un poco de teoría, pero no te arrepentirás.

Leave a comment!

23
Login or Register to join the conversation
Covi A. Avatar

i have appearance problem. can you please tell me what the actual problem
when your page show like this
your browser

my browser show like this
my browser

i am something confused. please clear me.

Reply

Hey Monoranjan,

Oh, haha, it's a tircky Google Chrome plugin that highlights JSON reponses :) It's called JSONView, you can install this extension for *your* browser and your JSON responses will look exactly like Ryan's ;) Btw, we mentioned this earlier in this course, or in previous courses, but I agree, it's easy to miss :) But now you know! Happy coding with nice JSON highlights now ;)

Cheers!

Reply
Covi A. Avatar

thank you very much for your answer. now it's working successfully

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | posted hace 2 años

Hi guys,

Hopefully you can give me a suggestion for my issue: I would like to start using a REST api in my existing project, however the problem is that it contains 300+ different entity types. For API-platform it seems not to be a problem, however the Swagger UI interface crashes my browser, just because of all not-yet-filled schemas (and endpoints for 2 entities as test). Do you a way to split the API in sections, documented on separate pages? Or is the project simply too big for Swagger UI?

Reply

Hey Thijs-jan V.,

Wow this looks like a BIG project, I'm not sure about how many entities can process default swagger connection, but there is a way to totally customise how it works. Probably it a little complex but it can be a good way. You can read about swagger here https://api-platform.com/do... I like the section about "Overriding the OpenAPI Specification"

PS BTW another tip is to use totally custom swagger it's not too difficult, but very cool to play =)

Cheers!

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

Hi,

at 8:08 you showed how to change the prefix.
Is it possible to change the prefix for the whole symfony app? so every single route even stuff like "/_profiler/ "?

I'm running multiple symfony applications parallel and I want each of them to have a distinct prefix so otherwise some features like the profile don't work.

For example I want that "localhost/users" is the true root of app1 not just localhost
and "localhost/cheeselisting" be the root of app2. So the path for the profile would be "localhost/user/_profiler" for the user app.

I have an ingress that decides where to route the request based on the path.
for example:
/users/* -> user server == every request that starts with /users will be passed the the user server<br /> /cheeses/* -> user server == every request that starts with /cheeses will be passed the the cheeses server

I hope I could explain it understandable ^^

Reply

Hey Ecludio,

Check the Kernel::configureRoutes() in your src/Kernel.php. That method imports all the routes into the system, that import() call allows you to set a prefix via 2nd argument, by default it's "/". I believe that's exactly what you need :)

Cheers!

Reply
Apr Avatar
Apr Avatar Apr | posted hace 2 años | edited

Hi!

Why is my api platform documentation empty?

When i go to the POST option, the body documentation is empty. After i click 'try it out' i only see an empty JSON {}. I had to write the JSON by hand following the example in the video like this:
{<br /> "title": "Cheese1",<br /> "description": "description 1",<br /> "price": 1000,<br /> "createdAt": "2021-02-02/22:32:23+00:00Z",<br /> "isPublished": true<br />}

And i get the following error:
<blockquote>"An exception occurred while executing 'INSERT INTO cheese_listing (title, description, price, created_at, is_published) VALUES (?, ?, ?, ?, ?)' with params [null, null, null, null, null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'title' cannot be null",</blockquote>

Why can't i see the documentation inside the body to send a POST request to create my first item? and why i'm getting 500 status after sending all "null"?

My Entity has the @ApiResource() and the fields are created as in the tutorial. I think i didn't miss any step, the migrations didn't throw any errors either.

Am i missing something?

Thanks!.

Reply

Hey Saul,

Hm, difficult to say, it looks like you missed something important from the tutorial. Did you download the course code and started from the start/ directory? Or are you following this course on your own project? Please, also, double-check the namespaces of the annotation you're using in your entities, probably you forgot the namespace or used incorrect one. I'd recommend you literally expand all the lines in the code block for the entity you're interested in and copy/paste it into your project. Does it help? Or you still don't see anything? Btw, are you in dev Symfony mode? Please, also try to clear the cache just in case! Does it help?

I really hope this helps!

Cheers!

Reply
Kristijan G. Avatar
Kristijan G. Avatar Kristijan G. | posted hace 2 años

Hi,

is there any way to change default scheme in Swagger UI to HTTP instead of HTTPS?

Thanks.

Reply

Hey Gets,

Do you run the project via "symfony serve" command? If so, try "symfony serve --no-tls", or probably better "symfony serve --allow-http" to allow both HTTP and HTTPS connections.

I hope this helps!

Cheers!

Reply
Default user avatar
Default user avatar isTom | posted hace 2 años | edited

Hello,

I have a weird unwanted addition called <b>additionalProp1</b> in all my entities docs Example Value
<br />{<br /> "@context": "string",<br /> "@id": "string",<br /> "@type": "string",<br /> "id": 0,<br /> "title": "string",<br /> "created_at": "2020-12-15T13:20:04.604Z",<br /> "updated_at": "2020-12-15T13:20:04.605Z",<br /> "additionalProp1": {}<br />}<br />

Have no idea where it coming from. In the Schema tab it is not present
<br />content_providers:jsonld-Read{description:ContentProviders@contextstring<br />readOnly: true@idstring<br />readOnly: true@typestring<br />readOnly: trueidinteger<br />readOnly: truetitle*string<br />nullable: truecreated_atstring($date-time)updated_atstring($date-time)}<br />

Reply

Hey @isTom

Does it only appear on your docs or it's also a field of your API endpoints?

Reply
Default user avatar

They only appear on /api/docs. API responses look ok.

Reply

Hey, sorry for my late response. If the API endpoints are ok, then I believe you added that extra information to your docs through any of the methods shown in this page https://api-platform.com/do...

Reply
Default user avatar
Default user avatar isTom | MolloKhan | posted hace 2 años | edited

Hello Diego,
I cannot identify any additional information in my class and sure there is nothing named <b>additionalProp1</b>
`
/**

  • ContentProviders
    *
  • @ORM\Table(name="content_providers",
  • indexes={
  • @ORM\Index(name="title", columns={"title"}),
  • },
  • uniqueConstraints={@ORM\UniqueConstraint(columns={"identification"})}
  • )
  • @UniqueEntity(fields={"identification"}, message="app.unique_value_required")
  • @ApiResource(
  • collectionOperations={"get", "post"},
  • itemOperations={
  • "get"={},
  • "put"
  • },
  • shortName="content_providers",
  • normalizationContext={"groups"={"content_providers:read"}, "swagger_definition_name"="Read"},
  • denormalizationContext={"groups"={"content_providers:write"}, "swagger_definition_name"="Write"},
  • )
  • @ORM\Entity
    */

class ContentProviders
{

/**
 * @var int
 *
 * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned"=true})
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="IDENTITY")
 * @Groups({"content_providers:read"})
 */
private $id;
/**
 * ContentProviders in the VodSource.
 *
 * @var VodSource
 *
 * @ORM\OneToMany(targetEntity="App\Entity\Main\VodSource", mappedBy="contentProvider")
 * @Groups({"content_providers:read"})
 **/
protected $vodSource;
/**
 * ContentProviders in the VodCreated.
 *
 * @var VodCreated
 *
 * @ORM\OneToMany(targetEntity="App\Entity\Main\VodCreated", mappedBy="contentProvider")
 **/
protected $vodCreated;
/**
 * @var string|null
 *
 * @ORM\Column(name="title", type="string")
 * @Groups({"content_providers:read", "content_providers:write"})
 * @Assert\NotBlank()
 */
private $title;
/**
 * @var string|null
 *
 * @ORM\Column(name="identification", type="string")
 * @Groups({"content_providers:read", "content_providers:write"})
 * @Assert\NotBlank()
 */
private $identification;
/**
 * @var \DateTime|null
 *
 * @ORM\Column(name="created_at", type="datetime", columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
 * @Groups({"content_providers:read"})
 */
private $createdAt;
/**
 * @var \DateTime|null
 *
 * @ORM\Column(name="updated_at", type="datetime", columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
 * @Groups({"content_providers:read"})
 */
private $updatedAt;

`

Reply

Ha! That's weird. What if you commit everything and then execute git grep additionalProp1 so you can track down where it's coming from

Cheers!

Reply
Default user avatar

Any way to disable "Try it out" buttons in Swagger UI?

Reply

Hey @Mike!

Hmm, I've never tried it! It looks like it's a 2 step process:

1) You disable it in JavaScript (since Swagger is a Js library) https://api-platform.com/do...

2) How do you get custom JS onto the page? By overriding/extending the template that renders Swagger: https://api-platform.com/do...

Let me know if it works!

Cheers!

Reply
Default user avatar
Default user avatar Kasper Smithsonian | posted hace 3 años | edited

`api_platform:

            resource: .
            type: api_platform
            prefix: /api

`
in api_plataform.yaml not work for me , I have to put in route.yaml of config

Reply

Hey Kasper Smithsonian

I believe you got confused by the file name. This api_platform.yaml files should live inside config/routes not inside config/packages

Cheers!

Reply
ddlzz Avatar

Hi!

Is there any way to redefine swagger operations description (like 'Retrieve foo collection') using an `@ApiResource` configuration?
I tried this
```
* @ApiResource(
* collectionOperations={
* "send": {
* "method": "POST",
* "path": "/email/send",
* "swaggerContext": {
* "summary": "test summary",
* "description": "sends a confirmation code",
* },
* "controller": SendEmailConfirmation::class,
* },
* },
```
but it doesn't work

Reply

Hey ddlzz

I haven't done what you are asking but I think you can do it by following this part of the documentation: https://api-platform.com/do...

I hope it helps. Cheers!

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