Laravel: Использование генераторов для пагинации

Источник: «Using Generators for Pagination»
Давайте попробуем использовать генераторы для постраничного разбиения всех записей на примере PokeAPI.

Вдохновлённый недавним твитом от @freekmurze, его будущей Mailcoach PHP SDK содержащей хороший API для разбиения на страницы всех записей:

// listing all subscribers of a list
$subscribers = $mailcoach->emailList('use-a-real-email-list-uuid-here')->subscribers();

do {
foreach($subscribers as $subscriber) {
echo $subscriber->email;
}
} while($subscribers = $subscribers->next())

Хотя мне определённо нравится идея метода ->next(), я думаю его можно упростить/улучшить, чтобы потребители могли просто перебирать записи, не беспокоясь о нумерации страниц.

На предыдущем мероприятии мы активно работали с API и поэтому часто получали большие объёмы данных разбивкой на страницы. Мы использовали аналогичные подходы, когда предоставляли сервисные классы, которые возвращали результаты и предоставляли методы для проверки/получения следующей страницы. Однако мы обнаружили, что это не всегда создаёт лучшие условия для разработчиков.

Вместо того чтобы заключать foreach в do/while цикл, что, если бы мы могли просто зациклить все результаты?

$subscribers = $mailcoach->emailList('use-a-real-email-list-uuid-here')->subscribers();

foreach($subscribers as $subscriber) {
echo $subscriber->email;
}

Несмотря на небольшое изменение в этом случае, мы полностью устранили необходимость в какой-либо логике для получения следующей страницы результатов.

Давайте посмотрим, как мы могли бы использовать генераторы для достижения этой цели.

Использование генераторов для постраничного разбиения всех записей

Чтобы воплотить это в жизнь, мне нравится использовать сервисный класс для инкапсуляции API и возврата записей.

В качестве простого примера давайте создадим сервисный класс для получения Покемонов из PokeAPI:

<?php

namespace App\Services;

use Generator;
use Illuminate\Support\Facades\Http;

class Pokemon
{
public function all($page = 1): Generator
{
do {
$response = Http::get('https://pokeapi.co/api/v2/pokemon', [
'offset' => ($page - 1) * 20,
'limit' => 20,
]);

foreach ($response['results'] as $result) {
yield $result;
}

$page++;
} while ($response['next'] !== null);
}
}

Используя генератор, возвращённый из класса Pokemon, мы можем перебирать всех покемонов без необходимости вручную извлекать дополнительные страницы. На мой взгляд, это создаёт гораздо более приятный опыт про потребителя, которому больше не нужно беспокоиться о получении следующей страницы результатов; они просто запрашивают всех покемонов и получают всех покемонов.

Чтобы увидеть, как мы будем использовать этот сервисный класс в действии, давайте запишем все результаты в наш терминал, когда запустим команду:

<?php

namespace App\Console\Commands;

use App\Services\Pokemon;
use Illuminate\Console\Command;

class ListPokemon extends Command
{
protected $signature = 'pokemon:list';

protected $description = 'Lists all the pokemon names, with support for providing name or type filters';

public function handle(Pokemon $pokemon)
{
foreach ($pokemon->all() as $pokemon) {
$this->info($pokemon['name']);
}

return Command::SUCCESS;
}
}

Я много работаю с Laravel и предпочитаю работать с коллекциями, а не с массивами. В Laravel есть концепция LazyCollection, которая может использовать наш генератор и предоставлять некоторые хорошие возможности Коллекций. Если вы не знакомы с LazeCollection, они очень похожи на обычны на Laravel коллекции, почти со всеми теми же методами.

Давайте обновим сервисный класс, что бы использовать LazyCollection:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\LazyCollection;

class Pokemon
{
public function all($page = 1): LazyCollection
{
return LazyCollection::make(
function () use ($page) {
do {
$response = Http::get('https://pokeapi.co/api/v2/pokemon', [
'offset' => ($page - 1) * 20,
'limit' => 20,
]);

foreach ($response['results'] as $result) {
yield $result;
}

$page++;
} while ($response['next'] !== null);
}
);
}
}

Теперь, когда мы возвращаем LazyCollection, давайте воспользуемся некоторыми замечательными возможностями коллекций, о которых я говорил, чтобы сделать реализацию более лаконичной:

<?php

namespace App\Console\Commands;

use App\Services\Pokemon;
use Illuminate\Console\Command;

class ListPokemon extends Command
{
protected $signature = 'pokemon:list';

protected $description = 'Lists all the pokemon names';

public function handle(Pokemon $pokemon)
{
$pokemon->all()
->each(fn (array $pokemon) => $this->info($pokemon['name']));

return Command::SUCCESS;
}
}

Хотя PokeAPI не поддерживает параметры запроса для фильтрации, если вы хотите предоставить метода для фильтрации, то я нахожу это довольно приятным:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\LazyCollection;

class Pokemon
{
private ?string $name = null;

private ?string $type = null;

public function name(?string $name): self
{
$this->name = $name;

return $this;
}

public function type(?string $type): self
{
$this->type = $type;

return $this;
}

public function all($page = 1): LazyCollection
{
return LazyCollection::make(
function () use ($page) {
do {
$response = Http::get('https://pokeapi.co/api/v2/pokemon', [
'offset' => ($page - 1) * 20,
'limit' => 20,
'name' => $this->name,
'type' => $this->type,
]);

foreach ($response['results'] as $result) {
yield $result;
}

$page++;
} while ($response['next'] !== null);
}
);
}
}

Мы создали несколько гибких методов для настройки имени и/или типа покемонов, которых мы хотим запросить, и благодаря HTTP-клиенту Laravel эти параметры будут игнорироваться, если они не заданы.

Если мы обновим предыдущую реализацию, мы могли бы использовать эти новые фильтры следующим образом:

<?php

namespace App\Console\Commands;

use App\Services\Pokemon;
use Illuminate\Console\Command;

class ListPokemon extends Command
{
protected $signature = 'pokemon:list {--name=} {--type=}';

protected $description = 'Lists all the pokemon names, with support for providing name or type filters';

public function handle(Pokemon $pokemon)
{
$pokemon->name($this->option('name'))
->type($this->option('type'))
->all()
->each(fn (array $pokemon) => $this->info($pokemon['name']));

return Command::SUCCESS;
}
}

Надеюсь эти примеры имеют смысл, и вы сможете увидеть преимущества использования этого подхода, чтобы помочь очистить ваши интерфейсы для работы с сервисными классами, которые взаимодействуют с API.

Дополнительные материалы

Предыдущая Статья

CSS: em и rem — всё, что нужно знать

Следующая Статья

Laravel: Где использовать middleware?