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 SubscribeEn sólo unos minutos, hemos dado a nuestros clientes de la API la posibilidad de filtrar por listados de quesos publicados y buscar por título y descripción. Puede que también necesiten la posibilidad de filtrar por precio. Eso parece un trabajo para... ¡ RangeFilter
! Añade otro @ApiFilter()
con RangeFilter::class
. Subamos inmediatamente y añadamos la declaración use
para eso: la del ORM. Luego,properties={"price"}
.
... lines 1 - 7 | |
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter; | |
... lines 9 - 14 | |
/** | |
* @ApiResource( | |
... lines 17 - 27 | |
* @ApiFilter(RangeFilter::class, properties={"price"}) | |
... line 29 | |
*/ | |
class CheeseListing | |
... lines 32 - 150 |
Este filtro es un poco loco. Dale la vuelta, refresca los documentos y mira la operación de recogida GET. ¡Vaya! Ahora tenemos un montón de casillas de filtro, para precio entre, mayor que, menor que, mayor o igual, etc. Busquemos todo lo que sea mayor que 20 y... Ejecuta. Esto añade ?price[gt]=20
a la URL. ¡Oh, excepto que eso es una búsqueda de todo lo que sea mayor de 20 céntimos! Prueba con 1000 en su lugar.
Esto devuelve sólo un elemento y, una vez más, anuncia los nuevos filtros dentro de hydra:search
.
Los filtros son súper divertidos. Hay montones de filtros incorporados, pero puedes añadir los tuyos propios. Desde un alto nivel, un filtro es básicamente una forma de modificar la consulta de Doctrine que se realiza cuando se obtiene una colección.
Hay otro filtro del que quiero hablar... y es un poco especial: en lugar de devolver menos resultados, se trata de devolver menos campos. Imaginemos que la mayoría de las descripciones son súper largas y contienen HTML. En el front-end, queremos poder obtener una colección de listados de quesos, pero sólo vamos a mostrar una versión muy corta de la descripción. Para que eso sea súper fácil, vamos a añadir un nuevo campo que devuelva esto. Busca getDescription()
y añade un nuevo método a continuación llamado public
function getShortDescription(). Esto devolverá una cadena anulable, en caso de que la descripción no esté establecida todavía. Añadamos inmediatamente esto a un grupo - cheese_listing:read
para que aparezca en la API.
... lines 1 - 30 | |
class CheeseListing | |
{ | |
... lines 33 - 90 | |
/** | |
* @Groups("cheese_listing:read") | |
*/ | |
public function getShortDescription(): ?string | |
{ | |
... lines 96 - 100 | |
} | |
... lines 102 - 160 | |
} |
En el interior, si el description
ya tiene menos de 40 caracteres, simplemente devuélvelo. En caso contrario, devuelve un substr
de la descripción -consigue los primeros 40 caracteres, y luego un pequeño ...
al final. Ah, y, en un proyecto real, para mejorar esto - probablemente deberías usar strip_tags()
en la descripción antes de hacer nada de esto para que no dividamos ninguna etiqueta HTML.
... lines 1 - 93 | |
public function getShortDescription(): string | |
{ | |
if (strlen($this->description) < 40) { | |
return $this->description; | |
} | |
return substr($this->description, 0, 40).'...'; | |
} | |
... lines 102 - 162 |
Actualiza los documentos... y luego abre la operación GET del artículo. Busquemos el id de listado de queso 1. Y... ¡ahí está! La descripción apenas supera los 40 caracteres. Copiaré la URL, la pondré en una nueva pestaña y añadiré .jsonld
al final para verlo mejor.
En este punto, añadir el nuevo campo no era nada especial. Pero... si algunas partes de mi frontend sólo necesitan el shortDescription
... es un poco inútil que la API envíe también el campo description
... ¡sobre todo si ese campo es muy, muy grande! ¿Es posible que un cliente de la API le diga a nuestra API que no devuelva determinados campos?
En la parte superior de nuestra clase, añade otro filtro con PropertyFilter::class
. Sube, escribe use PropertyFilter
y pulsa el tabulador para autocompletar. Esta vez, sólo hay una de estas clases.
... lines 1 - 9 | |
use ApiPlatform\Core\Serializer\Filter\PropertyFilter; | |
... lines 11 - 15 | |
/** | |
* @ApiResource( | |
... lines 18 - 29 | |
* @ApiFilter(PropertyFilter::class) | |
... line 31 | |
*/ | |
class CheeseListing | |
... lines 34 - 164 |
Este filtro tiene algunas opciones, pero funciona perfectamente sin hacer nada más.
Ve a refrescar nuestros documentos. Hmm, esto no supone ninguna diferencia aquí... no es una característica de nuestra API que pueda expresarse en el documento de especificaciones de la OpenAPI.
Pero, este recurso de nuestra API sí tiene un nuevo superpoder. En la otra pestaña, elige las propiedades exactas que quieres con?properties[]=title&properties[]=shortDescription
. ¡Dale caña! ¡Precioso! Seguimos obteniendo los campos JSON-LD estándar, pero entonces sólo recuperamos esos dos campos. Esta idea se denomina a veces "conjunto de campos dispersos", y es una forma estupenda de permitir que tu cliente de la API pida exactamente lo que quiere, sin dejar de organizarlo todo en torno a recursos concretos de la API.
Ah, y el usuario no puede intentar seleccionar nuevos campos que no formen parte de nuestros datos originales - no puedes intentar conseguir isPublished
- simplemente no funciona, aunque puedes habilitarlo.
Siguiente: hablemos de la paginación. Sí, ¡las APIs necesitan totalmente la paginación! Si tenemos 10.000 listados de quesos en la base de datos, no podemos devolverlos todos a la vez.
Ohh the String component, you're right, it would be super fancy to do it that way :)
Thanks for sharing it. Cheers!
Is it possible to specify properties[] on a POST request to control the data that is returned after success? A developer tried by POSTing the required data in the body and using the ?properties[] syntax, but instead of returning the fields he wanted, it seemed to filter out any data he had sent (that wasn't included in the properties array), making the POST payload invalid. Is that expected?
Hey AO!
A developer tried by POSTing the required data in the body and using the ?properties[] syntax, but instead of returning the fields he wanted, it seemed to filter out any data he had sent (that wasn't included in the properties array), making the POST payload invalid.
THAT is really interesting. I'm not sure if it's expected, but I do see (now) that this would happen. The PropertyFilter works by modifying the (de)normalization context - https://github.com/api-platform/core/blob/master/src/Serializer/Filter/PropertyFilter.php#L43 - and it is called both at the start of the request during read/deserialization AND later during serialization. Basically, you only want it to happen during deserialization, which totally makes sense to me :p. Unfortunately, that doesn't look possible currently - the filter isn't set up for this. You could request this as an option on the PropertyFilter (that would make sense to me), but it doesn't work that way currently.
One option might be to sub-class PropertyFilter, use that in your annotation, and override apply(). I've never done this before, but I can't think of why it wouldn't work. Then, in your overridden apply, check the $normalization
argument and ONLY call the parent apply() method if $normalization
is true.
I'm not 100% sure this will work, but it's worth a shot ;).
Cheers!
Thanks for the reply, super useful! I ended up adding a POST normalization group that only sends ID back for now but I might take a look at that solution out of work hours :D
Hi, Please how can I implement a filter that will filter my returned resources based on the available collections. For example the below returned data shows a collection property named 'exam'. I want the filter to return only the one with exam IRI property of "/api/exams/4". Thank you.
{
"@context": "/api/contexts/groups",
"@id": "/api/groups/fetch",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/api/groups/1",
"@type": "groups",
"groupName": "Narcotic Officer",
"exam": [
"/api/exams/4"
]
},
{
"@id": "/api/groups/2",
"@type": "groups",
"groupName": "Narcotic Assistant",
"exam": []
},
{
"@id": "/api/groups/3",
"@type": "groups",
"groupName": "Narcotic Officer2",
"exam": []
}
],
"hydra:totalItems": 3,
"hydra:search": {
"@type": "hydra:IriTemplate",
"hydra:template": "/api/groups/fetch{?}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": []
}
Hey Frederick E.!
Excellent question! Indeed, we didn't talk or think about how it might look to filter for a specific item (e.g. Group) based on the value/existence of some collection property. I just played with this, and fortunately, I think it's quite easy. I would:
1) On your Group entity, add a SearchFilter:
/**
* @ApiFilter(SearchFilter::class, properties={
* "exam": "exact"
* })
*/
class Group {
2) That's it! You should now see an option in Swagger to filter by this. You'll enter the IRI into the box. So, to only return "Groups" that have the exam /api/exams/4, it would look like https://localhost:9022/api/users?exam=%2Fapi%2Fgroups%2F4
- where the %2f
things are URL-encoded slashes.
Let me know if this helps!
Cheers!
Hi, when creating the getShortDescription() method, you indicate it "will return a nullable string, in case description isn't set yet", but in the code you didn't add the "?" for the return type of the method, right before "string". I wanted to update it myself but when I arrived in the GitHub repository, I just saw "[[[ code('5bacd98cdf') ]]]" and didn't know where to find this code. :/ It's not a very big issue, but it's worth fixing I think :p
Hey Ajie62!
You're right! Very nice catch! The video is right - the code is wrong. Thanks for trying to fix it... unfortunately (as you discovered!) this is one part of our process that is not contributor-friendly (I wish it were, but it's not). However, we're going to update that code block and add the nullable ? :)
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
}
}
2:40 We can do it more fancy way, using symfony/string component. https://symfony.com/doc/cur...