Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Extensión de consulta: Autofiltrar una colección

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

Cuando obtenemos una colección de tesoros, actualmente devolvemos todos los tesoros, incluso los inéditos. Probablemente algunos de ellos sean inéditos. Añadimos un filtro para controlar esto... pero seamos sinceros, no es la mejor solución. En realidad, necesitamos no devolver tesoros inéditos en absoluto.

Busca la Guía de actualización de la API Platform... y busca la palabra "estado" para encontrar una sección que habla de "proveedores" y "procesadores". Antes hemos hablado de los procesadores de estado, como el PersistProcessorde las operaciones Put y Post, que se encarga de guardar el artículo en la base de datos.

Proveedores de estado

Pero cada operación también tiene algo llamado proveedor de estado. Éste es el responsable de cargar el objeto o colección de objetos. Por ejemplo, cuando hacemos una petición GET para un único elemento, el ItemProvider es el responsable de tomar el ID y consultar la base de datos. También hay un CollectionProviderpara cargar una colección de elementos.

Así que si queremos ocultar automáticamente los tesoros no publicados, una opción sería decorar este CollectionProvider, de forma muy parecida a como hicimos con el PersistProcessor. Excepto... que eso no funcionará del todo. ¿Por qué? El CollectionProvider de Doctrine ejecuta la consulta y devuelve los resultados. Así que lo único que podríamos hacer es coger esos resultados... y luego ocultar los que no queramos. Eso... no es lo ideal para el rendimiento -imagínate cargar 50 tesoros y luego mostrar sólo 10- y confundiría la paginación. Lo que realmente queremos hacer es modificar la propia consulta: añadir unWHERE isPublished = true.

Probar el comportamiento

Por suerte para nosotros, este CollectionProvider "proporciona" su propio punto de extensión que nos permite hacer exactamente eso.

Antes de meternos de lleno, actualicemos una prueba para mostrar el comportamiento que queremos. BuscatestGetCollectionOfTreasures(). Toma el control de estos 5 tesoros y conviértelos todos en isPublished => true:

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 16
public function testGetCollectionOfTreasures(): void
{
DragonTreasureFactory::createMany(5, [
'isPublished' => true,
]);
... lines 22 - 44
}
... lines 46 - 183
}

porque ahora mismo, en DragonTreasureFactory, isPublished está configurado con un valor aleatorio:

... lines 1 - 29
final class DragonTreasureFactory extends ModelFactory
{
... lines 32 - 46
protected function getDefaults(): array
{
return [
... lines 50 - 51
'isPublished' => self::faker()->boolean(),
... lines 53 - 56
];
}
... lines 59 - 73
}

Luego añade uno más con createOne() y isPublished falsos:

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 16
public function testGetCollectionOfTreasures(): void
{
DragonTreasureFactory::createMany(5, [
'isPublished' => true,
]);
DragonTreasureFactory::createOne([
'isPublished' => false,
]);
... lines 25 - 44
}
... lines 46 - 183
}

¡Impresionante! Y aún queremos afirmar que esto devuelve sólo 5 elementos. Así que... asegurémonos de que falla:

symfony php bin/console phpunit --filter=testGetCollectionOfTreasures

Y... ¡sí! Devuelve 6 elementos.

Extensiones de la consulta de colección

Bien, para modificar la consulta de una ruta de colección, vamos a crear algo llamado extensión de consulta. En cualquier lugar de src/ - yo lo haré en el directorio ApiPlatform/- crea una nueva clase llamada DragonTreasureIsPublishedExtension. Haz que implemente QueryCollectionExtensionInterface, luego ve a "Código"->"Generar" oCommand+N en un Mac - y genera el único método que necesitamos: applyToCollection():

... lines 1 - 2
namespace App\ApiPlatform;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
class DragonTreasureIsPublishedExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
// TODO: Implement applyToCollection() method.
}
}

Esto está muy bien: nos pasa el $queryBuilder y algunos datos más. Luego, podemos modificar ese QueryBuilder. ¿Lo mejor? El QueryBuilderya tiene en cuenta cosas como la paginación y cualquier filtro que se haya aplicado. Así que no tenemos que preocuparnos de esas cosas.

Además, gracias al sistema de autoconfiguración de Symfony, sólo con crear esta clase y hacer que implemente esta interfaz, ¡ya será llamada cada vez que se utilice una ruta de colección!

Lógica de extensión de consulta

De hecho, se llamará para cualquier recurso. Así que lo primero que necesitamos esif (DragonTreasure::class !== $resourceClass) -afortunadamente nos pasa el nombre de la clase- y luego return:

... lines 1 - 7
use App\Entity\DragonTreasure;
... lines 9 - 10
class DragonTreasureIsPublishedExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
if (DragonTreasure::class !== $resourceClass) {
return;
}
... lines 18 - 21
}
}

A continuación, aquí es donde nos ponemos manos a la obra. Ahora, cada objeto QueryBuilder tiene un alias raíz que hace referencia a la clase o tabla que estás consultando. Normalmente, creamos el QueryBuilder... como desde dentro de un repositorio decimos algo como $this->createQueryBuilder('d') y d se convierte en ese "alias raíz". Luego lo utilizamos en otras partes de la consulta.

Sin embargo, en esta situación, no creamos el QueryBuilder, así que nunca elegimos ese alias raíz. Lo eligieron por nosotros. ¿Qué es? Es "plátano". En realidad, ¡no tengo ni idea de lo que es! Pero podemos conseguirlo con $queryBuilder->getRootAliases()[0]:

... lines 1 - 10
class DragonTreasureIsPublishedExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
if (DragonTreasure::class !== $resourceClass) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
... lines 20 - 21
}
}

Ahora es sólo lógica de consulta normal: $queryBuilder->andWhere() pasando sprintf(). Esto parece un poco raro: %s.isPublished = :isPublished, luego pasa $rootAliasseguido de ->setParameter('isPublished', true):

... lines 1 - 10
class DragonTreasureIsPublishedExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
if (DragonTreasure::class !== $resourceClass) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.isPublished = :isPublished', $rootAlias))
->setParameter('isPublished', true);
}
}

¡Genial! ¡Gira para probar esto!

symfony php bin/console phpunit --filter=testGetCollectionOfTreasures

¡Misión cumplida! Así de fácil.

¿Extensiones de consulta en los Subrecursos?

Por cierto, ¿funcionará también con los subrecursos? Por ejemplo, en nuestros documentos, también podemos obtener una colección de tesoros visitando/api/users/{user_id}/treasures. ¿Esto también ocultará los tesoros no publicados? La respuesta es... ¡sí! Así que no es algo de lo que debas preocuparte. No lo mostraré, pero esto también utiliza la extensión de consulta.

Ah, y si quieres que los usuarios administradores puedan ver los tesoros no publicados, puedes añadir una lógica que sólo modifique esta consulta si el usuario actual no es administrador.

A continuación: ¡esta extensión de consulta arregla la ruta de recogida! Pero... alguien aún podría obtener un único tesoro no publicado directamente por su id. ¡Vamos a arreglarlo!

Leave a comment!

0
Login or Register to join the conversation
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