Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Paginación y accesorios de fundició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

Vamos a empezar a hacer más cosas con nuestra API... ¡así que es hora de darle vida a esto con algunos data fixtures!

Para ello, me gusta utilizar Foundry junto con DoctrineFixturesBundle. Así que ejecuta

composer require foundry orm-fixtures --dev

para instalar ambos como dependencias de dev. Cuando termine, ejecuta

php bin/console make:factory

Añadir la fábrica de Foundry

Si no has utilizado Foundry antes, para cada entidad, creas una clase de fábrica que sea realmente buena para crear esa entidad. Yo le daré a cero para generar la deDragonTreasure.

El resultado final es un nuevo archivo src/Factory/DragonTreasureFactory.php:

... lines 1 - 2
namespace App\Factory;
use App\Entity\DragonTreasure;
use App\Repository\DragonTreasureRepository;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;
/**
* @extends ModelFactory<DragonTreasure>
*
* @method DragonTreasure|Proxy create(array|callable $attributes = [])
* @method static DragonTreasure|Proxy createOne(array $attributes = [])
* @method static DragonTreasure|Proxy find(object|array|mixed $criteria)
* @method static DragonTreasure|Proxy findOrCreate(array $attributes)
* @method static DragonTreasure|Proxy first(string $sortedField = 'id')
* @method static DragonTreasure|Proxy last(string $sortedField = 'id')
* @method static DragonTreasure|Proxy random(array $attributes = [])
* @method static DragonTreasure|Proxy randomOrCreate(array $attributes = [])
* @method static DragonTreasureRepository|RepositoryProxy repository()
* @method static DragonTreasure[]|Proxy[] all()
* @method static DragonTreasure[]|Proxy[] createMany(int $number, array|callable $attributes = [])
* @method static DragonTreasure[]|Proxy[] createSequence(array|callable $sequence)
* @method static DragonTreasure[]|Proxy[] findBy(array $attributes)
* @method static DragonTreasure[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
* @method static DragonTreasure[]|Proxy[] randomSet(int $number, array $attributes = [])
*/
final class DragonTreasureFactory extends ModelFactory
{
... lines 32 - 36
public function __construct()
{
parent::__construct();
}
... lines 41 - 46
protected function getDefaults(): array
{
return [
'coolFactor' => self::faker()->randomNumber(),
'description' => self::faker()->text(),
'isPublished' => self::faker()->boolean(),
'name' => self::faker()->text(255),
'plunderedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'value' => self::faker()->randomNumber(),
];
}
... lines 58 - 61
protected function initialize(): self
{
return $this
// ->afterInstantiate(function(DragonTreasure $dragonTreasure): void {})
;
}
protected static function getClass(): string
{
return DragonTreasure::class;
}
}

Esta clase es realmente buena creando objetos DragonTreasure. ¡Incluso tiene un montón de bonitos datos aleatorios listos para ser utilizados!

Para hacerlo aún más elegante, voy a pegar un poco de código que he dragonizado. Ah, y también necesitamos una constante TREASURE_NAMES... que también pegaré encima. Puedes coger todo esto del bloque de código de esta página.

... lines 1 - 29
final class DragonTreasureFactory extends ModelFactory
{
private const TREASURE_NAMES = ['pile of gold coins', 'diamond-encrusted throne', 'rare magic staff', 'enchanted sword', 'set of intricately crafted goblets', 'collection of ancient tomes', 'hoard of shiny gemstones', 'chest filled with priceless works of art', 'giant pearl', 'crown made of pure platinum', 'giant egg (possibly a dragon egg?)', 'set of ornate armor', 'set of golden utensils', 'statue carved from a single block of marble', 'collection of rare, antique weapons', 'box of rare, exotic chocolates', 'set of ornate jewelry', 'set of rare, antique books', 'giant ball of yarn', 'life-sized statue of the dragon itself', 'collection of old, used toothbrushes', 'box of mismatched socks', 'set of outdated electronics (such as CRT TVs or floppy disks)', 'giant jar of pickles', 'collection of novelty mugs with silly sayings', 'pile of old board games', 'giant slinky', 'collection of rare, exotic hats'];
... lines 33 - 46
protected function getDefaults(): array
{
return [
'coolFactor' => self::faker()->numberBetween(1, 10),
'description' => self::faker()->paragraph(),
'isPublished' => self::faker()->boolean(),
'name' => self::faker()->randomElement(self::TREASURE_NAMES),
'plunderedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTimeBetween('-1 year')),
'value' => self::faker()->numberBetween(1000, 1000000),
];
}
... lines 58 - 72
}

Bien, esta clase ya está lista. Segundo paso: para crear realmente algunos accesorios, abresrc/DataFixtures/AppFixtures.php. Borraré el método load(). Todo lo que necesitamos es: DragonTreasureFactory::createMany(40) para crear un buen botín de 40 tesoros:

... lines 1 - 2
namespace App\DataFixtures;
use App\Factory\DragonTreasureFactory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
DragonTreasureFactory::createMany(40);
}
}

¡Probemos esto! De vuelta a tu terminal, ejecuta:

symfony console doctrine:fixtures:load

Y... ¡parece que ha funcionado! Volvamos a nuestros documentos de la API, actualicemos... y probemos la ruta de recolección GET. Pulsa ejecutar.

¡Ya tenemos Paginación!

¡Qué guay! ¡Mira todos esos preciosos tesoros! Recuerda que hemos añadido 40. Pero si te fijas bien... aunque IDs no empiece por 1, podemos ver que aquí hay definitivamente menos de 40. La respuesta dice hydra:totalItems: 40, pero sólo muestra 25.

Aquí abajo, este hydra:view explica un poco por qué: ¡hay paginación integrada! Ahora mismo estamos viendo la página 1... y también podemos ver las URL de la última página y de la página siguiente.

Así que sí, las rutas API que devuelven una colección necesitan paginación... igual que un sitio web. Y con API Platform, simplemente funciona.

Para jugar con esto, vamos a /api/treasures.jsonld. Ésta es la página 1... y luego podemos añadir ?page=2 para ver la página 2. Es lo más fácil que haré en todo el día.

Profundizando en la configuración de la API Platform

Ahora, si lo necesitas, puedes cambiar un montón de opciones de paginación. Veamos si podemos ajustar el número de elementos por página de 25 a 10.

Para empezar a indagar en la configuración, abre tu terminal y ejecuta:

php bin/console debug:config api_platform

Hay muchas cosas que puedes configurar en API Platform. Y este comando nos muestra la configuración actual. Así, por ejemplo, puedes añadir un title y un description a tu API. Esto pasa a formar parte de la OpenAPI Spec... y así aparece en tu documentación.

Si buscas pagination -no queremos la que está bajo graphql... queremos la que está bajo collection - podemos ver varias opciones relacionadas con la paginación. Pero, de nuevo, esto nos está mostrando la configuración actual... no nos muestra necesariamente todas las claves posibles.

Para verlo, en lugar de debug:config, ejecuta:

php bin/console config:dump api_platform

debug:config te muestra la configuración actual. config:dump te muestra un árbol completo de configuraciones posibles. Ahora... vemos pagination_items_per_page. ¡Eso parece lo que queremos!

Esto es realmente genial. Todas estas opciones viven bajo algo llamadodefaults. Y son versiones en forma de serpiente de exactamente las mismas opciones que encontrarás dentro del atributo ApiResource. Establecer cualquiera de estas defaults en la configuración hace que ese sea el valor por defecto que se pasa a esa opción para cada ApiResourcede tu sistema. Genial.

Así que, si quisiéramos cambiar los elementos por página globalmente, podríamos hacerlo con esta configuración. O, si queremos cambiarlo sólo para un recurso, podemos hacerlo sobre la clase.

Personalizar el número máximo de elementos por página

Busca el atributo ApiResource y añade paginationItemsPerPage ajustado a 10:

... lines 1 - 18
#[ApiResource(
... lines 20 - 34
paginationItemsPerPage: 10
)]
class DragonTreasure
{
... lines 39 - 161
}

De nuevo, puedes ver que las opciones que ya tenemos... están incluidas en la configuración de defaults.

Muévete y vuelve a la página 1. Y... ¡voilà! Una lista mucho más corta. Además, ahora hay 4 páginas de tesoros en lugar de 2.

Ah, y para tu información: también puedes hacer que el usuario de tu API pueda determinar cuántos elementos mostrar por página mediante un parámetro de consulta. Consulta la documentación para saber cómo hacerlo.

Bien, ahora que tenemos un montón de datos, vamos a añadir la posibilidad de que nuestros usuarios de la API Dragón busquen y filtren entre los tesoros. Por ejemplo, tal vez un dragón esté buscando un tesoro de caramelos envueltos individualmente entre todo este botín. Eso a continuación.

Leave a comment!

11
Login or Register to join the conversation
Mickael-M Avatar
Mickael-M Avatar Mickael-M | posted hace 4 meses

I can't play the video (lesson 12), this is the error : The media could not be loaded, either because the server or network failed or because the format is not supported.

Reply

Hey Mickael,

We're sorry to hear you're experiencing trouble accessing this video. This happens sometimes when the video was on pause for a long time. The full page refresh should help. If not - try to download the video and watch it locally. I just double-checked the video - it works great for me, so it might be a temporary outage of Vimeo - the hosting provider behind our videos - or something related to your local network. Anyway, downloading the video and watching it locally should help. Let us know if you still have problems with it!

Cheers!

Reply
Mickael-M Avatar

I cleared cache and now it works :)

Reply

Beautiful - sorry again about that!

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted hace 5 meses

What do I need to do to have a custom paginator (or DataProvider??) so that I can pass an offset and limit instead of items_per_page and a page_number?

Javascript code that I have no control over expects a slice of the data, not a page.

As a hack, I passed a "start" parameter (which will of course be seen as a filter), and replaced the offset (leaving items_per_page along, as that functions as the limit too). This is exactly what I want -- to control the starting offset, not the page.

Doctrine\Orm\Extension\PaginationExtension,php

    [$offset, $limit] = $pagination;
    $offset = $context['filters']['start'];

More details at https://github.com/api-platform/core/issues/4672 -- I'd LOVE to be able to solve this problem, as I have a hack that does two larger-than-necessary API calls and then slices the result as needed. Ugh.

Reply

Hey @Tac-Tacelosky

Perhaps you need a cursor or partial pagination, you can learn more about them in the docs https://api-platform.com/docs/core/pagination/#cursor-based-pagination
Also, remember that you can "decorate" APIPlatform services to extend/modify their behavior

Cheers!

Reply
Marko Avatar

Hi!

I am working on an API Platform 3 (Symfony 6) app.

In my JSON response, I have the following :

{ ... "totalItems": 7065, "itemsPerPage": 10, ... }

Is it possible to change the config (or do something else) so that I get :

{ ... "total_items": 7065, "page_size": 10, ... }

So basically I want to rename these fields, in the JSON response. Is it possible ?

I emphasize that I want to override the default fields (totalItems and itemsPerPage) in the JSON response.
Thanks!

Reply

Hey Marko,

I couldn't find any config to change those field names in the documentation. So, what I think you could do is create an event listener and override those fields directly on the response data. https://api-platform.com/docs/core/events/

Cheers!

Reply
Marko Avatar

Hello! Thanks a lot for your answer. Actually, according to this link, GraphQL is not supported by the event system. Given that we may actually migrate from REST to GraphQL in the future, do you think there is another way to override these field names ? Because, with GraphQL, what we implemented with events will not work anymore. Maybe we can use some normalizer or anything else, compatible with GraphQL, to achieve the same goal ?..

Reply
Marko Avatar
Marko Avatar Marko | Marko | posted hace 6 meses | edited

It actually worked with a custom normalizer. According to API Platform doc, unlike event system, normalizers & denormalizers are GraphQL friendly

1 Reply

Hey Marko,

Oh, I didn't know that about GraphQL. Anyways, I'm glad to know you could solve your problem.
By the way, remember that you can "decorate" any services to add extra behavior to them https://api-platform.com/docs/core/extending/#leveraging-the-built-in-infrastructure-using-composition

Cheers!

1 Reply
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.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
    }
}
userVoice