If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login Subscribe¡Vaya! ¡Cuidado! ¡Capítulo extra! Sabemos que nuestra API está completamente descrita mediante la especificación Open API. Incluso podemos verlo visitando /api/docs.json
. Esto muestra todas nuestras rutas y sus campos. Obtiene esta deliciosa información leyendo nuestro código, PHPdoc y otras cosas. Y sabemos que esto se utiliza para alimentar la página de documentos Swagger UI. Nuestra API también se describe mediante JSON-LD e Hydra.
Y ambos tipos de documentación de la API pueden utilizarse para otras cosas.
Por ejemplo, busca "react admin" para encontrar un sistema de administración de código abierto basado en React. Esto es súper potente y genial... y existe desde hace mucho tiempo. Y la forma en que funciona es... increíble: lo apuntamos a nuestra documentación de la API y luego... ¡se construye solo! Creo que deberíamos probarlo.
Busca "api platform react admin" para encontrar la página de documentación de la API Platform que trata de esto. Tiene algo de información... pero lo que realmente buscamos está aquí. Haz clic en "Empezar". Esto nos guía a través de todos los detalles, incluyendo incluso la configuración CORS si tienes ese problema.
Así que... ¡hagámoslo!
Si utilizas la distribución Docker de API Platform, esta área de administración viene preinstalada, pero también es bastante fácil añadirla manualmente. Ahora mismo, nuestra aplicación no tiene JavaScript, así que tenemos que arrancarlo todo. Busca tu terminal y ejecuta:
composer require encore
Esto instala WebpackEncoreBundle... y su receta nos da una configuración básica del frontend. Una vez hecho esto, instala los activos de Node con:
npm install
Bien, vuelve a los documentos. API Platform tiene su propio paquete Node que ayuda a integrarse con el administrador. Así que vamos a instalarlo. Copia la líneanpm install
-también puedes usar yarn
si quieres-, pégala en el terminal y añade un -D
al final.
npm install @api-platform/admin -D
Ese -D
no es super importante, pero yo suelo instalar mis activos como devDependencies
.
Para que todo esto funcione, en última instancia, vamos a renderizar un único componente React en una página. Para ayudarte con eso, voy a instalar un paquete UX que es... realmente bueno renderizando componentes React. Es opcional, pero bueno.
Ejecuta:
composer require symfony/ux-react
Perfecto. Ahora, gira y busca "symfony ux react" para encontrar su documentación. Copia este código de instalación: tenemos que añadirlo a nuestro archivo app.js
... aquí enassets/
. Pégalo... y no necesitamos todos estos comentarios. También moveré este código debajo de las importaciones.
... lines 1 - 12 | |
import './bootstrap'; | |
registerReactControllerComponents(require.context('./react/controllers', true, /\.(j|t)sx?$/)); |
¡Increíble! Esto básicamente dice que buscará en un directorio assets/react/controllers/
y hará que cada componente React que haya dentro sea súper fácil de renderizar en Twig. Así que, vamos a crearlo: en assets/
, añade dos nuevos directorios:react/controllers/
. Y luego crea un nuevo archivo llamado ReactAdmin.jsx
.
Para el contenido, vuelve a los documentos de la API Platform... y nos da casi exactamente lo que necesitamos. Copia esto... y pégalo dentro de nuestro nuevo archivo. Pero antes, no lo parece, pero gracias a la sintaxis JSX, estamos utilizando React, así que necesitamos un import React from 'react'
.
Y... asegurémonos de que lo tenemos instalado:
npm install react -D
En segundo lugar, echa un vistazo al prop entrypoint
. Esto es genial. Pasamos la URL a nuestra página de inicio de la API... y luego React admin se encarga del resto. Para nosotros, esta URL sería algo como https://localhost:8000/api
. Pero... Prefiero no codificar "localhost" en mi JavaScript.
En lugar de eso, vamos a pasarlo como una propiedad. Para permitirlo, añade un argumento props
... y luego di props.entrypoint
.
import { HydraAdmin } from "@api-platform/admin"; | |
import React from 'react'; | |
export default (props) => ( | |
<HydraAdmin entrypoint={props.entrypoint} /> | |
); |
¿Cómo lo introducimos? Lo veremos en un minuto.
Muy bien, veamos si el sistema llega a construirse. Enciéndelo:
npm run watch
Y... ¡error de sintaxis! Ve esta sintaxis .jsx
y... ¡no sabe qué hacer con ella! Eso es porque aún no hemos activado React dentro de WebpackEncore. Pulsa Ctrl+C para detenerlo... luego gira y abre webpack.config.js
. Busca un comentario que diga .enableReactPreset()
. Ahí lo tienes. Descomenta eso.
... lines 1 - 8 | |
Encore | |
... lines 10 - 64 | |
// uncomment if you use React | |
.enableReactPreset() | |
... lines 67 - 77 |
Ahora, cuando volvamos a ejecutar
npm run watch
de nuevo... ¡seguirá sin funcionar! Pero nos da el comando que necesitamos para instalar el único paquete que falta para que React sea compatible Cópialo y ejecútalo:
npm install @babel/react-preset@^7.0.0 --save-dev
Y ahora cuando probemos
npm run watch
... ¡funciona! Es hora de renderizar ese componente React.
¿Cómo lo hacemos? Esta es la parte fácil. En src/Controller/
, crea una nueva clase PHP llamada AdminController
. Probablemente será el controlador más aburrido que jamás hayas creado. Haz que extienda AbstractController
, y luego añade un public function
llamado dashboard()
, que devolverá un Response
, aunque eso es opcional. Encima de esto, añade un Route()
para /admin
.
Todo lo que necesitamos dentro es return $this->render()
y luego una plantilla: admin/dashboard.html.twig
.
... lines 1 - 2 | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminController extends AbstractController | |
{ | |
'/admin') ( | |
public function dashboard(): Response | |
{ | |
return $this->render('admin/dashboard.html.twig'); | |
} | |
} |
¡Genial! Abajo, en el directorio templates/
, crea ese directorio admin/
... y dentro, un nuevo archivo llamado dashboard.html.twig
. De nuevo, ésta es probablemente una de las plantillas más aburridas que harás nunca, al menos al principio. Amplíabase.html.twig
y añade block body
y endblock
.
Ahora, ¿cómo renderizamos el componente React? Gracias a ese paquete UX React, es superfácil. Crea el elemento en el que debe renderizarse y añadereact_component()
seguido del nombre del componente. Como el archivo se llamaReactAdmin.jsx
en el directorio react/controllers/
, su nombre será ReactAdmin
.
{% extends 'base.html.twig' %} | |
{% block body %} | |
<div {{ react_component('ReactAdmin', { | |
... line 5 | |
}) }}></div> | |
{% endblock %} |
Y aquí es donde pasamos los accesorios. Recuerda: tenemos uno llamado entrypoint
. Ah, pero déjame arreglar mi sangría... y recuerda añadir el </div>
. No necesitamos nada dentro del div... porque ahí es donde aparecerá mágicamente el área de administración de React, como un conejo salido de una chistera.
Pasa el prop set entrypoint
a la función normal path()
. Ahora, sólo tenemos que averiguar el nombre de ruta que API Platform utiliza para la página de inicio de la API. Esta pestaña está ejecutando npm... así que abriré una nueva pestaña de terminal y la ejecutaré:
php bin/console debug:router
¡Woh! Demasiado grande. Así está mejor. Desplázate un poco hacia arriba y... aquí está. Queremos:api_entrypoint
. Vuelve y pásalo.
{% extends 'base.html.twig' %} | |
{% block body %} | |
<div {{ react_component('ReactAdmin', { | |
entrypoint: path('api_entrypoint') | |
}) }}></div> | |
{% endblock %} |
¡Momento de la verdad! Busca tu navegador, cambia la dirección a /admin
, y... ¡hola ReactAdmin! ¡Woh! Entre bastidores, eso hizo una petición a nuestro punto de entrada de la API, vio todos los diferentes recursos de la API que tenemos, ¡y creó este admin! Lo sé, ¿no es una locura?
No profundizaremos demasiado en esto, aunque puedes personalizarlo y casi seguro que necesitarás personalizarlo. No es perfecto: parece un poco confuso por nuestro dragonTreasures
incrustado, pero ya es muy potente. ¡Incluso la validación funciona! Observa: cuando envío, lee la validación del lado del servidor devuelta por nuestra API y asigna cada error al campo correcto. Y los tesoros conocen nuestros filtros. ¡Todo está aquí!
Si esto te parece interesante, no dudes en seguir investigando.
¡Muy bien, equipo! ¡Lo habéis conseguido! Has superado el primer tutorial sobre la API Platform, que es fundamental para todo. Ahora entiendes cómo se serializan los recursos, cómo se relacionan los recursos con otros recursos, los IRI, etc. Todas estas cosas te van a servir para cualquier API que estés construyendo. En el próximo tutorial, hablaremos de usuarios, seguridad, validación personalizada, campos específicos de usuario y otras cosas extravagantes. Cuéntanos qué estás construyendo y, si tienes alguna pregunta, estamos a tu disposición en la sección de comentarios.
¡Muy bien, amigos! ¡Hasta la próxima!
To fix this I changed the composer.json to include "minimum-stability": "beta". Then I did a composer update
.
Hey FR,
At what point did you get that error? Did you download the course code, or you're following the course by yourself?
I did download the course code and coded along. The error popped up in this chapter or the end of the last chapter.
Hey @FR!
There definitely IS some weird stuff happening right now, as the ecosystem transitions away from annotation - e.g. https://github.com/symfony/symfony/issues/48792
But I can't get this to repeat using the code - I've tried doing composer install
on the finish
code as well as a composer update
. We MIGHT have a dependency that needs upgrading, but I can't trigger it. @FR - do you see the error when you run composer instal
or do you need to actually use an endpoint?
Thanks!
Hi, i'm user a project with Symfony 6.3.1, all work fine, but in ReactAdmin i have this error on my console :
adminDataProvider.js:27 Uncaught Error: Cannot fetch API documentation:
Unable to find the URL for "https://localhost:8000/api/docs.jsonld#Entrypoint/treasure" in the entrypoint, make sure your API resource has at least one GET collection operation declared.
Have you verified that CORS is correctly configured in your API?
at adminDataProvider.js:27:1
I found the problem, i added my 2nd ApiResource before the first on my DragonTreasure class. I know now we have to add ApiResource AFTER the first one
#[ApiResource(
uriTemplate: '/users/{user_id}/treasures{._format}',
shortName: 'Treasure',
operations: [
new GetCollection()
],
uriVariables: [
'user_id' => new Link(
fromProperty: 'dragonTreasures',
fromClass: User::class
)
],
normalizationContext: [
'groups' => ['treasure:read']
]
)]
Hello!
In my admin panel when I go to create a user it only requires the username but not the password and the email, also when I want to create or modify an object it always gives me the error 422 because one of my assertions jumps without me having touched that field.
Any ideas?
Hi @Fran!
Hmm.
In my admin panel when I go to create a user it only requires the username but not the password and the email,
Do you mean that it doesn't show your email / password fields? Or that it shows them, but they are not required?
always gives me the error 422 because one of my assertions jumps without me having touched that field.
What do you mean by "the assertion jumps"?
But, I can maybe give some hints :). If you haven't done it already, password
will need a validation group to be added - https://symfonycasts.com/screencast/api-platform-security/validation-groups - so that it isn't always required. This may or may not be your issue, but I wanted to mention it :).
Cheers!
Hello,
How can i fetch owner(data who owner is email from token) data if i use JWT token ?
Regards.
Mmx
Hey @Mepcuk!
So you have a JWT and that JWT contains the email
of the user? Is that correct? We'll talk closer to this use-case (though not this exactly) in the next tutorial. But I would:
A) Use the new access_token
system
B) In the "access token handler" class you'll create, you'll decode the JWT to get the email
C) Then, return new UserBadge($email)
. As long as your have an "entity" user provider in security.yaml
set up to query from the email
property... that's all you need.
Let me know if that helps :).
Cheers!
Hey @weaverryan!
I think you did not understand my question - I have a Get operation -
#[Get(
normalizationContext: ['groups' => ['loan-person:read']],
security: "is_granted('ROLE_USER') and object.getEmail() == user.getEmail()",
securityMessage: "You don't have permission to view content",
)]
It's work perfect and i can get information if i know ID :))) If not resricted.
But i want to make resourse where i can get all data (relations) related to this user (i login via JWT token)
I try to get Collection but it not worked
#[GetCollection(
normalizationContext: ['groups' => ['loan-person:read']],
security: "is_granted('ROLE_USER') and collection.getEmail() == user.getEmail()",
securityMessage: "You don't have permission to view content in bulk",
)]
Security key did not described in API=platform docs and i don't know how to fetch collection assigned-related to user with this JWT token. You told that in security: possible to write user check, but how?
Hey @Mepcuk!
I think you did not understand my question
Haha, that happens to me a lot - apologies :)
Let's see... Question: how are you using GetCollection
operation? Are you using an ApiFilter
to filter by email - e.g. /api/loans?email=foo@example.com
? Or something else?
When you use a GetCollection
endpoint, the actual "object" is not, of course, a single LoanPerson
object, but an array (or technically a Collection
) or LoanPerson
objects. You tried using collection.getEmail()
- but I don't quite understand yet what you were trying to do.
You also said:
i don't know how to fetch collection assigned-related to user with this JWT token.
If I understand correctly, you would like to be able to make a GET /api/loans
and receive back only the loans "owned" by the currently-authenticated user. Is that correct? If so, solving this is less about security and more about filtering the data (from a security perspective, all users will have access to fetch their collection of loans, but they should only see their own loans). For this, I would use a "query extension" - https://symfonycasts.com/screencast/api-platform-security/query-extension - to automatically filter this. It doesn't matter that the user is inside of a JWT. It only matters that you fetch the currently-authenticated user, then modify the query based on that user inside the query extension.
Let me know if I was closer this time :)
Cheers!
// 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
}
}
If you are also running into this error:
removing 'doctrine/annotations' might fix it: