gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
La clave detrás de cómo API Platform convierte nuestros objetos en JSON... y también de cómo transforma JSON de nuevo en objetos es el Serializador de Symfony. symfony/serializer
es un componente independiente que puedes utilizar fuera de API Platform y es increíble. Le das cualquier entrada -como un objeto u otra cosa- y lo transforma en cualquier formato, como JSON
, XML
o CSV
.
Como puedes ver en este elegante diagrama, sigue dos pasos. Primero, toma tus datos y los normaliza en una matriz. En segundo lugar, los codifica en el formato final. También puede hacer lo mismo a la inversa. Si partimos de JSON, como si enviáramos JSON a nuestra API, primero lo descodifica en una matriz y luego lo desnormaliza de nuevo en un objeto.
Para que todo esto ocurra, internamente hay muchos objetos normalizadores distintos que saben cómo trabajar con datos diferentes. Por ejemplo, hay unDateTimeNormalizer
que es realmente bueno manejando objetos DateTime
. Compruébalo: nuestra entidad tiene un campo createdAt
, que es un objeto DateTime
:
... lines 1 - 26 | |
class DragonTreasure | |
{ | |
... lines 29 - 48 | |
#[ORM\Column] | |
private ?\DateTimeImmutable $plunderedAt = null; | |
... lines 51 - 130 | |
} |
Si te fijas en nuestra API, cuando probamos la ruta GET
, ésta se devuelve como una cadena especial de fecha y hora. El DateTimeNormalizer
es el responsable de hacerlo.
También hay otro normalizador muy importante llamado ObjectNormalizer
. Su trabajo consiste en leer las propiedades de un objeto para poder normalizarlas. Para ello, utiliza otro componente llamado property-access
. Ese componente es inteligente.
Por ejemplo, si miramos nuestra API, cuando hacemos una petición GET a la ruta de recogida, uno de los campos que devuelve es name
. Pero si nos fijamos en la clase,name
es una propiedad privada:
... lines 1 - 26 | |
class DragonTreasure | |
{ | |
... lines 29 - 33 | |
#[ORM\Column(length: 255)] | |
private ?string $name = null; | |
... lines 36 - 130 | |
} |
Entonces, ¿cómo demonios se lee eso?
Ahí es donde entra en juego el componente PropertyAccess
. Primero mira si la propiedadname
es pública. Y si no lo es, busca un método getName()
:
... lines 1 - 26 | |
class DragonTreasure | |
{ | |
... lines 29 - 33 | |
#[ORM\Column(length: 255)] | |
private ?string $name = null; | |
... lines 36 - 59 | |
public function getName(): ?string | |
{ | |
return $this->name; | |
} | |
... lines 64 - 130 | |
} |
Así que eso es lo que se llama realmente cuando se construye el JSON.
Lo mismo ocurre cuando enviamos JSON, por ejemplo para crear o actualizar un DragonTreasure
. PropertyAccess examina cada campo del JSON y, si ese campo se puede establecer, por ejemplo mediante un método setName()
, lo establece:
... lines 1 - 26 | |
class DragonTreasure | |
{ | |
... lines 29 - 33 | |
#[ORM\Column(length: 255)] | |
private ?string $name = null; | |
... lines 36 - 59 | |
public function getName(): ?string | |
{ | |
return $this->name; | |
} | |
public function setName(string $name): self | |
{ | |
$this->name = $name; | |
return $this; | |
} | |
... lines 71 - 130 | |
} |
Y lo que es aún mejor: ¡incluso buscará métodos getter o setter que no se correspondan con ninguna propiedad real! Puedes utilizar esto para crear campos extra" en tu API que no existen como propiedades en tu clase.
¡Vamos a probarlo! Imagina que, cuando estamos creando o editando un tesoro, en lugar de enviar un campo description
, queremos poder enviar un campo textDescription
que contenga texto sin formato... pero con saltos de línea. Luego, en nuestro código, transformaremos esos saltos de línea en etiquetas HTML <br>
.
Te mostraré lo que quiero decir. Copia el método setDescription()
. Luego, debajo, pega y llama a este nuevo método setTextDescription()
. Básicamente va a establecer la propiedad description
... pero antes llama a nl2br()
. Esa función transforma literalmente las nuevas líneas en etiquetas <br>
. Si llevas por aquí tanto tiempo como yo, recordarás cuando nl2br
era superguay:
... lines 1 - 26 | |
class DragonTreasure | |
{ | |
... lines 29 - 83 | |
public function setTextDescription(string $description): self | |
{ | |
$this->description = nl2br($description); | |
return $this; | |
} | |
... lines 90 - 137 | |
} |
De todos modos, sólo con ese cambio, actualiza la documentación y abre las rutas POST o PUT. Y.. ¡Tenemos un nuevo campo llamado textDescription
! ¡Sí! El serializador ha visto el método setTextDescription()
y ha determinado que textDescription
es una propiedad virtual "definible"
Sin embargo, no lo vemos en la ruta GET. ¡Y eso es perfecto! No existe el método getTextDescription()
, por lo que aquí no habrá un nuevo campo. El nuevo campo es escribible, pero no legible.
Vamos a probar esta ruta Primero... Tengo que ejecutar la ruta de recolección GET para ver qué identificadores tenemos en la base de datos. Perfecto: tengo un Tesoro con ID 1. Cierra esto. Vamos a probar la ruta PUT para hacer nuestra primera actualización. Cuando utilizas la ruta PUT, no necesitas enviar todos los campos: sólo los que quieras cambiar.
Pasa textDescription
... e incluiré \n
para representar algunas líneas nuevas en JSON.
Cuando lo probemos, ¡sí! código de estado 200. Y fíjate: ¡el campo description
tiene esas etiquetas <br>
!
Vale, ahora que tenemos setTextDescription()
... quizá sea la única forma en que queremos permitir que se establezca ese campo. Para imponerlo, erradica el método setDescription()
.
Ahora, cuando actualizamos... y miramos la ruta PUT, ¡todavía tenemos textDescription
, pero el campo description
ha desaparecido! El serializador se ha dado cuenta de que ya no es configurable y lo ha eliminado de nuestra API. Seguiría siendo devuelto porque es algo que podemos leer, pero ya no es escribible.
Todo esto es realmente increíble. Simplemente nos preocupamos de escribir nuestra clase como queremos y luego API Platform construye nuestra API en consecuencia.
Vale, ¿qué más? Bueno, es un poco raro que podamos establecer el campo createdAt
: normalmente se establece interna y automáticamente. Vamos a arreglarlo.
Pero, ¿sabes qué? Quería llamar a este campo plunderedAt
. Refactorizaré y cambiaré el nombre de esa propiedad... y dejaré que PhpStorm cambie también el nombre de mis métodos getter y setter.
¡Genial! Esto también hará que cambie la columna de mi base de datos... así que gira a tu consola y ejecuta:
symfony console make:migration
Viviré peligrosamente y lo ejecutaré inmediatamente:
symfony console doctrine:migrations:migrate
¡Listo! Gracias a ese cambio de nombre... en la API, excelente: el campo es ahoraplunderedAt
.
Vale, olvídate de la API por un momento: vamos a hacer un poco de limpieza. La finalidad de este campo plunderedAt
es que se establezca automáticamente cada vez que creemos un nuevo DragonTreasure
.
Para ello, crea un public function __construct()
y, dentro, ponthis->plunderedAt = new DateTimeImmutable()
. Y ahora no necesitamos el = null
en la propiedad.
... lines 1 - 26 | |
class DragonTreasure | |
{ | |
... lines 29 - 48 | |
#[ORM\Column] | |
private \DateTimeImmutable $plunderedAt; | |
... lines 51 - 54 | |
public function __construct() | |
{ | |
$this->plunderedAt = new \DateTimeImmutable(); | |
} | |
... lines 59 - 128 | |
} |
Y si buscamos setPlunderedAt
, en realidad ya no necesitamos ese método, ¡elimínalo!
Esto significa ahora que la propiedad plunderedAt
es legible pero no escribible. Así que, no te sorprendas, cuando actualizamos y abrimos la ruta PUT
o POST
, plunderedAt
está ausente. Pero si miramos el aspecto que tendría el modelo si obtuviéramos un tesoro, plunderedAt
sigue ahí.
Muy bien, ¡un objetivo más! Vamos a añadir un campo virtual llamado plunderedAtAgo
que devuelva una versión legible por humanos de la fecha, como "hace dos meses". Para ello, tenemos que instalar un nuevo paquete:
composer require nesbot/carbon
Una vez que termine... busca el método getPlunderedAt()
, cópialo, pégalo debajo, devolverá un string
y llámalo getPlunderedAtAgo()
. Dentro, devuelveCarbon::instance($this->getPlunderedAt()))
y luego ->diffForHumans()
.
... lines 1 - 11 | |
use Carbon\Carbon; | |
... lines 13 - 27 | |
class DragonTreasure | |
{ | |
... lines 30 - 118 | |
/** | |
* A human-readable representation of when this treasure was plundered. | |
*/ | |
public function getPlunderedAtAgo(): string | |
{ | |
return Carbon::instance($this->plunderedAt)->diffForHumans(); | |
} | |
... lines 126 - 137 | |
} |
Así que, como ahora entendemos, no hay ninguna propiedad plunderedAtAgo
... pero elserializer
debería ver esto como legible a través de su getter y exponerlo. Ah, y ya que estoy aquí, añadiré un poco de documentación arriba para describir el significado del campo.
Bien, probemos esto. En cuanto actualizamos y abrimos una ruta GET
, ¡vemos el nuevo campo en el ejemplo! También podemos ver los campos que recibiremos abajo, en la sección Esquemas. Volvamos atrás, probemos la ruta GET
con el ID one
. Y... ¿a que mola?
A continuación: ¿qué pasa si queremos tener ciertos métodos getter o setter en nuestra clase, como setDescription()
, pero no queremos que formen parte de nuestra API? La respuesta: grupos de serialización.
Hey @TronZin
That's unexpected. Could you double-check if that method still exists in the Carbon library? You might have got a newer version and they renamed the method. If that's not the case, your IDE may not have indexed the library correctly
Hi,
when I installed nesbot/carbon then browser shows an error:
Attempted to load class "Locale" from the global namespace.<br />Did you forget a "use" statement for "Symfony\Component\Validator\Constraints\Locale"?
Adding use statment does not helps :( any ideas?
Hey Szymon,
I believe you haven't installed the php-intl
module in your local environment. If you're on Ubuntu, you can do this sudo apt-get install php-intl
Cheers!
I had installed intl extension but it required to uncomment lines in several places in php/php.ini (I have Windows 10)
I see... well, as far as I know Windows is not very friendly for developing purposes. That's why I use Windows WSL, it's not perfect but it gets the job done :)
// 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
}
}
Heya, for some reason visual studio code marks "Carbon" as an error in the getPlunderedAtAgo() method that its used here.