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 SubscribeBusca en Google el serializador de Symfony y encuentra una página llamada El componente serializador.
La Plataforma API está construida sobre los componentes Symfony. ¡Y todo el proceso de cómo convierte nuestro objeto CheeseListing
en JSON... y JSON de nuevo en un objetoCheeseListing
, lo hace el Serializador de Symfony! Si entendemos cómo funciona, ¡estamos en el negocio!
Y, al menos en la superficie, es maravillosamente sencillo. Mira el diagrama que muestra cómo funciona. Pasar de un objeto a JSON se llama serialización, y de JSON a un objeto se llama deserialización. Para ello, internamente, pasa por un proceso llamado normalización: primero toma tu objeto y lo convierte en una matriz. Y luego lo codifica en JSON o en el formato que sea.
En realidad, hay un montón de clases "normalizadoras" diferentes que ayudan en esta tarea, como una que es muy buena para convertir los objetos de DateTime
en una cadena y viceversa. Pero la clase principal -la que está en el centro de este proceso- se llama ObjectNormalizer
. Entre bastidores, utiliza otro componente de Symfony llamado PropertyAccess
, que tiene un superpoder: si le das un nombre de propiedad, como title
, es realmente bueno para encontrar y utilizar métodos getter y setter para acceder a esa propiedad.
En otras palabras, cuando la plataforma API intenta "normalizar" un objeto en una matriz, ¡utiliza los métodos getter y setter para hacerlo!
Por ejemplo, ve que hay un método getId()
, y así, lo convierte en una claveid
en el array... y finalmente en el JSON. Hace lo mismo congetTitle()
- que se convierte en title
. ¡Es así de sencillo!
Cuando enviamos datos, ¡hace lo mismo! Como tenemos un método setTitle()
, podemos enviar JSON con una clave title
. El normalizador tomará el valor que enviamos, llamará a setTitle()
¡y lo pasará!
Es una forma sencilla, pero muy útil, de permitir que tus clientes de la API interactúen con tu objeto, tu recurso de la API, utilizando sus métodos getter y setter. Por cierto, el componente PropertyAccess también admite propiedades públicas, hassers, issers, adders, removers - básicamente un montón de convenciones de nomenclatura de métodos comunes, además de getters y setters.
De todos modos, ahora que sabemos cómo funciona esto, ¡somos súper peligrosos! En serio, ahora podemos enviar un campo description
. Imaginemos que esta propiedad puede contener HTML en la base de datos. Pero la mayoría de nuestros usuarios no entienden realmente el HTML y, en su lugar, se limitan a escribir en un cuadro con saltos de línea. Vamos a crear un nuevo campo personalizado llamado textDescription
. Si un cliente de la API envía un campo textDescription
, convertiremos las nuevas líneas en saltos de línea HTML antes de guardarlo en la propiedaddescription
.
¿Cómo podemos crear un campo de entrada personalizado totalmente nuevo para nuestro recurso? Busca setDescription()
, duplícalo y llámalo setTextDescription()
. Dentro de, por ejemplo, $this->description = nl2br($description);
. Es un ejemplo tonto, pero incluso olvidando la Plataforma API, esto es una buena y aburrida codificación orientada a objetos: hemos añadido una forma de establecer la descripción si quieres que las nuevas líneas se conviertan en saltos de línea.
... lines 1 - 18 | |
class CheeseListing | |
{ | |
... lines 21 - 83 | |
public function setTextDescription(string $description): self | |
{ | |
$this->description = nl2br($description); | |
return $this; | |
} | |
... lines 90 - 125 | |
} |
Pero ahora, actualiza y abre de nuevo la operación POST. ¡Vaya! ¡Dice que todavía podemos enviar un campo description
, pero también podemos pasar textDescription
! Pero si intentas la operación GET... seguimos obteniendo sólo description
.
¡Eso tiene sentido! Hemos añadido un método setter -que permite enviar este campo- pero no hemos añadido un método getter. También puedes ver el nuevo campo descrito abajo en la sección de modelos.
Pero, probablemente no queramos permitir que el usuario envíe tanto description
como textDescription
. Es decir, se podría, pero es un poco raro: si el cliente enviara ambos, chocarían entre sí y la última clave ganaría porque su método setter sería llamado en último lugar. Así que vamos a eliminar setDescription()
.
Actualiza ahora. ¡Me encanta! Para crear o actualizar un listado de quesos, el cliente enviarátextDescription
. Pero cuando recojan los datos, siempre obtendrán de vuelta description
. De hecho, probemos... con el id 1. Abre la operación PUT y establece textDescription
en algo con algunos saltos de línea. Sólo quiero actualizar este campo, así que podemos eliminar los demás campos. Y... ¡a ejecutar! código de estado 200 y... ¡un campo description
con algunos saltos de línea!
Por cierto, el hecho de que nuestros campos de entrada no coincidan con los de salida está totalmente bien. La coherencia está muy bien, y pronto te mostraré cómo podemos arreglar esta incoherencia. Pero no hay ninguna regla que diga que tus datos de entrada tienen que coincidir con los de salida.
Bien, ¿qué más podemos hacer? Bueno, tener un campo createdAt
en la salida está muy bien, pero probablemente no tenga sentido permitir que el cliente lo envíe: el servidor debería establecerlo automáticamente.
¡No hay problema! ¿No quieres que se permita el campo createdAt
en la entrada? Busca el métodosetCreatedAt()
y elimínalo. Para autoconfigurarlo, vuelve a la buena y antigua programación orientada a objetos. Añade public function __construct()
y, dentro, $this->createdAt = new \DateTimeImmutable()
.
... lines 1 - 54 | |
public function __construct() | |
{ | |
$this->createdAt = new \DateTimeImmutable(); | |
} | |
... lines 59 - 118 |
Ve a actualizar los documentos. Sí, aquí ha desaparecido... pero cuando intentamos la operación GET, sigue estando en la salida.
¡Estamos en racha! ¡Así que vamos a personalizar una cosa más! Digamos que, además del campo createdAt
-que está en este formato feo, pero estándar-, también queremos devolver la fecha como una cadena -algo así como 5 minutes ago
o 1 month ago
.
Para ayudarnos a hacerlo, busca tu terminal y ejecuta
composer require nesbot/carbon
Esta es una práctica utilidad de DateTime que puede darnos fácilmente esa cadena. Ah, mientras esto se instala, volveré a la parte superior de mi entidad y eliminaré elpath
personalizado en la operación get
. Es un ejemplo genial... pero no hagamos que nuestra API sea rara sin motivo.
... lines 1 - 7 | |
/** | |
* @ApiResource( | |
... line 10 | |
* itemOperations={ | |
* "get"={}, | |
... line 13 | |
* }, | |
... line 15 | |
* ) | |
... line 17 | |
*/ | |
... lines 19 - 118 |
Sí, eso se ve mejor.
De vuelta a la terminal.... ¡hecho! En CheeseListing
, busca getCreatedAt()
, pasa por debajo, y añade public function getCreatedAtAgo()
con un tipo de retorno string
. Luego,return Carbon::instance($this->getCreatedAt())->diffForHumans()
.
... lines 1 - 106 | |
public function getCreatedAtAgo(): string | |
{ | |
return Carbon::instance($this->getCreatedAt())->diffForHumans(); | |
} | |
... lines 111 - 124 |
Ya sabes lo que hay que hacer: con sólo añadir un getter, cuando actualizamos... y miramos el modelo, tenemos un nuevo createdAtAgo
- ¡campo de sólo lectura! Y, por cierto, también sabe que description
es de sólo lectura porque no tiene ningún setter.
Desplázate hacia arriba y prueba la operación de recogida GET. Y... genial: createdAt
ycreatedAtAgo
.
Por muy bonito que sea controlar las cosas simplemente ajustando tus métodos getter y setter, no es lo ideal. Por ejemplo, para evitar que un cliente de la API establezca el campo createdAt
, tuvimos que eliminar el método setCreatedAt()
. Pero, ¿qué pasa si, en algún lugar de mi aplicación -como un comando que importa listados de queso heredados- necesitamos establecer manualmente la fecha createdAt
? Vamos a aprender a controlar esto con los grupos de serialización.
Hey Will T.!
Sorry for the slow reply! Hmm, try setting a "Content-Type" header to "application/json" in your Ajax call. I'm thinking that you're sending JSON, but because the Ajax request doesn't have a Content-Type, Api Platform thinks it's HTML, and then can't deserializer it.
Let me know if that helps!
Cheers!
The way $this->createdAt = new \DateTimeImmutable();
works out okay feels like a bit of a mystery.
Hello, are you touching the topic of custom business logic (not related to the entities) at some point of the course?
I'm still wondering should I use API Platforms for those or not?
For instance, I should make business calculations not related to the database at all - based on input API should return some value - but I would like to keep swagger documentation for API calls.
Is that even possible?
Thanks
Hi Nenad M.!
It's not something that we touch on in this tutorial - we have that topic planned for a future (but not date yet) 3rd part to this series :). For these business calculation stuff - the "perfect" solution is to create a DTO class and map *that* as your ApiResource. This would require:
A) To live in the Entity/ directory or (if in a different directory) for you to update your api_platform.yaml config file to also point to this directory
B) A custom data persister and custom data provider (to save and load data). Actually, if you aren't *writing* any data (no PUT, POST or DELETE operations) then you don't even need the data persister. For the data "provider", that would be where you do whatever calculations you need and then populate that data on the DTO.
I know that's a bit of a vague answer, but let me know if it makes sense :).
Cheers!
You are creating a field createdAtAgo
. But this will only work when being in the same timezone I guess or am I wrong? Also when it's longer than 24 hours ago it probably won't make a big difference anymore.
Hey Paul!
Actually, this is one of the great things about "ago" fields: they're always correct, regardless of timezone. Suppose something were created right now at 2020-06-24 02:31:00
UTC. That date will get stored in the database. 3 hours from now (so, 2020-06-24 05:31:00
), that date will be loaded from the database. Then, the server (your PHP code) will compare the current data in UTC to the data from the database and ultimately return something like "3 hours ago" in your API. So, it works perfectly :).
When this does not work perfectly is if you tried to do this in JavaScript - e.g. you return 2020-06-24 02:31:00
from your server, and then convert that to an "ago" time in JavaScript. In that case, if you're not careful, you might compare the date using the user's timezone on their computer. But with this pure server-side approach, you're safe.
Cheers!
Oh yes that is true. I didn't think about that thanks 🙈.
And Carbon probably can also handle the language so you would pass the client language in a header field?
But how would you handle the refreshing then in the client. Let's say the site stays open for a few minutes. Then the displayed data would be incorrect.
Hey Hanswer01,
If you want to translate into a different language - yes, you can pass the locale to the date, see locale() method in docs.
> But how would you handle the refreshing then in the client. Let's say the site stays open for a few minutes. Then the displayed data would be incorrect.
Yes, that's the downside. That obviously will show fresh data only on the page load. So, users will need to refresh the page to see the difference, or if it's a problem for you - you would need to go with a more complex solution. I think JS would help the best here, i.e. you would need to change that "ago" string with JS constantly.
Cheers!
Thanks a lot for the detailed explanation.
Yes but then again if I have a multilingual site I have to care about a lot of things when increasing. But it's probably not that big of a deal to just show the correct ago time on load.
Yeah, multilingual website complicates things. I think so too... but depends on your project of course. Also, you can look at some websites around to see how they solve this problem. For example, GitHub shows dates in "ago" format, and looks like they use JS to update that date without page refreshing.
Cheers!
With Symfony 4.3.1 I still get this error when loading the `/api` page:
```
An exception has been thrown during the rendering of a template ("Property "textDescription" does not exist in class "App\Entity\CheeseListing"
```
However, if I change the name of the property to 'textDescription' (rather than 'description'), it works.
Sry.. I did not see the 'Show more replies' link :( so it looked like the issue was still open. thx!
symfony 4.3.0 throws following error when running adding setTextDescription() method:
`
composer require nesbot/carbon
...
In FileLoader.php line 166:
Property "textDescription" does not exist in class "App\Entity\CheeseListin
!! g" in . (which is being imported from "/Volumes/SanDisk/Documents/code/symf
!! ony/api-platform/config/routes/api_platform.yaml"). Make sure there is a lo
!! ader supporting the "api_platform" type.
In PropertyMetadata.php line 40:
Property "textDescription" does not exist in class "App\Entity\CheeseListing"
`
I was wondering if I was the only one having this issue... I've got the same issue here. How can we fix that, please? [Update] When I tried the part with getCreatedAt, I was surprise to see that it works. So now I'm just wondering why it works with this and not with textDescription...
Hey guys,
The new version of Symfony is came out - v4.3.1. Could you please upgrade to it and say if it fixes the problem?
Cheers!
Hey Ajie62 and @Azeem!
So here's what's going on :). In Symfony 4.3, a new feature called "auto validation" is added (well, you only get it if you start a new 4.3 project - the new recipe enables it: https://github.com/symfony/...
This has a bug :). It's more or less this issue: https://github.com/symfony/...
For now, you'll need to disable the auto validation feature (just comment out those two lines in validator.yaml). I'm going to push that issue forward so we can get it fixed!
Cheers!
Hi Ryan,
Thanks for answering! I've commented out the two lines you mentioned in validator.yaml and now it's working properly. Hopefully the issue isn't too hard and will be fixed soon. :) Have a very good day!
Thanks for the report! I hadn't hit this yet, so you've helped us fix it faster :). Here is the PR that, hopefully, is the correct solution: https://github.com/symfony/...
Cheers!
// 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
}
}
I'm trying to update a record from an ajax call to a put API-Platform endpoint, but I'm getting a message: "Deserialization for the format \"html\" is not supported." Can anyone help me with this?