Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Personalizar los documentos de OpenAPI

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Para utilizar tokens de API en Swagger, tenemos que escribir la palabra "Bearer" (portador) y luego el token. ¡Lamentable! Sobre todo si pretendemos que lo utilicen usuarios reales. ¿Cómo podemos solucionarlo?

La especificación OpenAPI es la clave

Recuerda que Swagger se genera enteramente a partir del documento de especificaciones OpenAPI que construye API Platform. Puedes ver este documento consultando la fuente de la página -puedes verlo todo ahí mismo- o yendo a /api/docs.json. Hace unos minutos, hemos añadido una configuración a API Platform llamada Authorization:

api_platform:
... lines 2 - 7
swagger:
api_keys:
access_token:
name: Authorization
type: header
... lines 13 - 18

El resultado final es que ha añadido estas secciones de seguridad aquí abajo. Sí, es así de sencillo: esta configuración activó estas nuevas secciones en este documento JSON: nada más. Swagger entonces lee eso y sabe que debe hacer que esta "Autorización" esté disponible.

Así que indagué un poco directamente en el sitio de OpenAPI y descubrí que sí tiene una forma de definir un esquema de autenticación en el que no necesitas pasar manualmente la parte del "Portador". Desgraciadamente, a menos que me lo esté perdiendo, la configuración de API Platform no permite añadirlo. Entonces, ¿hemos terminado? De ninguna manera, y por una razón increíble.

Crear nuestro OpenApiFactory

Para crear este documento JSON, internamente, API Platform crea un objeto OpenApi, rellena todos estos datos en él y luego lo envía a través del serializador de Symfony. Esto es importante porque podemos modificar el objeto OpenApi antes de que pase por el serializador. ¿Cómo? El objeto OpenApi se crea a través de un núcleoOpenApiFactory... y podemos decorarlo.

Compruébalo: en el directorio src/, crea un nuevo directorio llamadoApiPlatform/... y dentro, una nueva clase PHP llamada OpenApiFactoryDecorator. Haz que implemente OpenApiFactoryInterface. Luego ve a "Código"->"Generar" oCommand+N en un Mac para implementar el único método que necesitamos: __invoke():

... lines 1 - 2
namespace App\ApiPlatform;
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\OpenApi;
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
public function __invoke(array $context = []): OpenApi
{
// TODO: Implement __invoke() method.
}
}

¡Hola Servicio Decoración!

Ahora mismo, existe un servicio central OpenApiFactory en la API Platform que crea el objeto OpenApi con todos estos datos. Éste es nuestro astuto plan: vamos a decirle a Symfony que utilice nuestra nueva clase como OpenApiFactoryen lugar de la del núcleo. Pero... definitivamente no queremos reimplementar toda la lógica del núcleo. Para evitarlo, también le diremos a Symfony que nos pase el núcleo original OpenApiFactory.

Puede que te resulte familiar lo que estamos haciendo. Es la decoración de clases: una estrategia orientada a objetos para extender clases. Es muy fácil de hacer en Symfony y API Platform lo aprovecha mucho.

Siempre que hagas decoración, crearás un constructor que acepte la interfaz que estás decorando. Así que OpenApiFactoryInterface. Lo llamaré$decorated. Y déjame poner private delante de eso:

... lines 1 - 4
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
... lines 6 - 9
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
public function __construct(private OpenApiFactoryInterface $decorated)
{
}
... lines 15 - 23
}

Perfecto.

Aquí abajo, para empezar, di $openApi = $this->decorated y luego llama al método __invoke()pasándole el mismo argumento: $context:

... lines 1 - 9
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
... lines 12 - 15
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
... lines 19 - 22
}
}

Eso llamará a la fábrica del núcleo que hará todo el trabajo duro de crear el objeto OpenApi completo. Aquí abajo, devuelve eso:

... lines 1 - 9
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
... lines 12 - 15
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
... lines 19 - 21
return $openApi;
}
}

¿Y entre medias? Sí, ¡ahí es donde podemos liarnos! Para asegurarnos de que esto funciona, por ahora, simplemente vuelca el objeto $openApi:

... lines 1 - 9
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
... lines 12 - 15
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
dump($openApi);
return $openApi;
}
}

El atributo #[AsDecorator]

En este momento, desde un punto de vista orientado a objetos, esta clase está configurada correctamente para la decoración. Pero el contenedor de Symfony sigue configurado para utilizar elOpenApiFactory normal: no va a utilizar nuestro nuevo servicio en absoluto. De alguna manera tenemos que decirle al contenedor que, en primer lugar, el servicio principal OpenApiFactory debe ser sustituido por nuestro servicio, y en segundo lugar, que el servicio principal original debe pasarse a nosotros.

¿Cómo podemos hacerlo? Encima de la clase, añade un atributo llamado #[AsDecorator] y pulsa tabulador para añadir esa declaración use. Pásale el id de servicio del núcleo originalOpenApiFactory. Puedes indagar un poco para encontrarlo o normalmente la documentación te lo dirá. En realidad, API Platform documenta la decoración de este servicio, así que en sus documentos encontrarás que el identificador del servicio es api_platform.openapi.factory:

... lines 1 - 6
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
#[AsDecorator('api_platform.openapi.factory')]
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
... lines 12 - 23
}

¡Eso es! Gracias a esto, cualquiera que antes utilizara el servicio principal deapi_platform.openapi.factory recibirá en su lugar nuestro servicio, pero nos pasará el original.

Así que... ¡debería funcionar! Para probarlo, dirígete a la página principal de la API y actualízala. ¡Sí! Cuando esta página se carga, renderiza el documento JSON de OpenAPI en segundo plano. ¡El volcado en la barra de herramientas de depuración web demuestra que ha dado con nuestro código! Y fíjate en ese precioso objeto OpenApi: lo tiene todo, incluido security, que coincide con lo que vimos en el JSON. Así que ahora, ¡podemos retocarlo!

Personalizar la configuración OpenAPI

El código que voy a poner aquí es un poco específico del objeto OpenApi y de la configuración exacta que sé que necesitamos en el JSON final de la Open API:

... lines 1 - 9
#[AsDecorator('api_platform.openapi.factory')]
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
... lines 13 - 16
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject();
... lines 22 - 26
return $openApi;
}
}

Obtenemos el objeto $securitySchemes, y luego anulamos access_token. Esto coincide con el nombre que utilizamos en la configuración. Establece un nuevo objeto SecurityScheme() con dos argumentos con nombre: type: 'http' y scheme: 'bearer':

... lines 1 - 5
use ApiPlatform\OpenApi\Model\SecurityScheme;
... lines 7 - 9
#[AsDecorator('api_platform.openapi.factory')]
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
... lines 13 - 16
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject();
$securitySchemes['access_token'] = new SecurityScheme(
type: 'http',
scheme: 'bearer',
);
return $openApi;
}
}

¡Ya está! Primero actualiza el documento JSON sin procesar para que podamos ver qué aspecto tiene. Déjame buscar "Portador". ¡Ya está! ¡Hemos modificado el aspecto del JSON!

¿Qué opina Swagger de esta nueva configuración? Actualiza y pulsa "Autorizar". Genial: access_token, http, Bearer. Ve a robar un token de API... pégalo sin decir Bearer primero y dale a "Autorizar". Probemos la misma ruta. Uy, tengo que darle a "Probar". Y... ¡precioso! Mira esa cabecera Authorization! Nos ha pasado Bearer. Misión cumplida.

Por cierto, podrías pensar, dado que estamos anulando por completo la configuración deaccess_token, que podríamos simplemente eliminarla de api_platform.yaml. Por desgracia, por razones sutiles que tienen que ver con cómo se genera la documentación de seguridad, seguimos necesitándola. Pero diré# overridden in OpenApiFactoryDecorator:

api_platform:
... lines 2 - 7
swagger:
api_keys:
# overridden in OpenApiFactoryDecorator
access_token:
... lines 12 - 19

Esto era sólo un ejemplo de cómo podrías ampliar tu documento de especificaciones de la API Abierta. Pero si alguna vez necesitas modificar algo más, ahora ya sabes cómo.

A continuación, hablemos de los ámbitos.

Leave a comment!

2
Login or Register to join the conversation
Oleh-K Avatar

Maybe something has been changed in API Platform with updates, since this video was recorded. I've tried, and securitySchemes hasn't been overridden.

        $securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject();
        $securitySchemes['access_token'] = new SecurityScheme(
            type: 'http',
            scheme: 'bearer',
        );

After some workaround I've stopped on this solution:

    public function __invoke(array $context = []): OpenApi
    {
        $decorated = $this->factory->__invoke($context);

        $components = $decorated->getComponents()->withSecuritySchemes(
            new ArrayObject(
                [
                    'JWT' => new SecurityScheme(
                        type:         'http',
                        description:  'Value for the JWT Authorization header parameter.',
                        scheme:       'bearer',
                        bearerFormat: 'JWT'
                    )
                ]
            )
        );

        return $decorated->withComponents($components);
    }
Reply

Hey @Oleh-K!

Sorry for the slow reply! Hmm. I wonder: did you add the access_token config to api_platform.yaml? I actually think your solution is superior. My guess (I could be wrong) is that you don't have this config. And so, this line:

$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject();

is using the new \ArrayObject part. But in my code, because I have the config, the $openApi->getComponents()->getSecuritySchemes() is returning an ArrayObject. The difference is that, in my case, I'm them "mutating" this existing object. But in your case, your "mutation" the new \ArrayObject(), which is then never actually set into the OpenApi object. Basically, my code was short-sighted and would ONLY work in the case where $openApi->getComponents()->getSecuritySchemes() DOES return an ArrayObject.

Am I correct? Does $openApi->getComponents()->getSecuritySchemes() return an ArrayObject or is it null? Anyway, your solution is superior, I believe, as it will work in call cases.

Cheers!

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.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice