Laravel: Использование генераторов для пагинации
Вдохновлённый недавним твитом от @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.