Реальные примеры использования генераторов в PHP

Реальные примеры использования генераторов в PHP

Реальные примеры использования генераторов в PHP

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

«Да, генераторы, безусловно, выглядят здорово, но знаете… за исключением вычисления последовательности Фибоначчи, я не вижу, чем они могут быть мне полезны.»

И в чем-то такие люди правы, ведь даже примеры в документации PHP о генераторах довольно упрощены. Они только показывают, как эффективно реализовать диапазон или итерацию по строкам файла.

Но даже из этих простых примеров мы можем понять суть генераторов: это просто упрощенные итераторы.

Генератор позволяет вам написать код, который использует foreach для итерации по набору данных без необходимости создавать массив в памяти.

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

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

Чтобы иметь возможность продавать эти электронные книги и отображать соответствующую информацию для наших клиентов, нам необходимо иметь множество метаданных для наших книг (название, формат, цена, издатель, автор(ы), …).

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

Итак, начнем!

Итерация больших наборов данных
Для первого примера предположим, что у меня есть большая коллекция электронных книг, и я хочу отфильтровать те, которые можно читать в веб-читалке.

Традиционно я бы написал что-то вроде этого:

private function getEbooksEligibleToWebReader($ebooks)
{
$rule = ‘format = «EPUB» AND protection != «Adobe DRM»‘;
$filteredEbooks = [];

foreach ($ebooks as $ebook) {
if ($this->rulerz->satisfies($ebook, $rule)) {
$filteredEbooks[] = $ebook;
}
}

return $filteredEbooks;
}
Проблема здесь очевидна: чем больше у меня бесплатных электронных книг, тем больше переменная $filteredEbooks будет занимать памяти.

Решением могло бы стать создание итератора, который бы перебирал $ebooks и возвращал те, которые соответствуют требованиям. Но для этого пришлось бы создать новый класс, а итераторы немного утомительны в написании… К счастью, начиная с PHP 5.5 мы можем использовать генераторы!

private function getEbooksEligibleToWebReader($ebooks)
{
$rule = ‘format = «EPUB» AND protection != «Adobe DRM»‘;

foreach ($ebooks as $ebook) {
if ($this->rulerz->satisfies($ebook, $rule)) {
yield $ebook;
}
}
}
Да, рефакторинг метода getEbooksEligibleToWebReader для использования генераторов так же прост, как замена присваивания $filteredEbooks на оператор yield.

Если предположить, что это не массив со всеми электронными книгами, а итератор или генератор (еще лучше!), то потребление памяти будет постоянным, независимо от количества книг, и мы обязательно найдем эти книги тогда, когда они нам понадобятся.

Агрегирование данных с нескольких источников
Теперь давайте рассмотрим часть, связанную с извлечением. Я не говорил вам, но эти электронные книги поступают из разных источников данных: реляционной базы данных Mysql и Elasticsearch.

Мы можем написать простой метод для объединения этих двух источников:

private function getEbooks()
{
$ebooks = [];

// fetch from the DB
$stmt = $this->db->prepare(«SELECT * FROM ebook_catalog»);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);

foreach ($stmt as $data) {
$ebooks[] = $this->hydrateEbook($data);
}

// and from Elasticsearch (findAll uses ES scan/scroll)
$cursor = $this->esClient->findAll();

foreach ($cursor as $data) {
$ebooks[] = $this->hydrateEbook($data);
}

return $ebooks;
}
Но опять же, объем памяти, используемый этим методом, слишком сильно зависит от количества электронных книг, которые мы имеем в базе данных и в Elasticsearch.

Мы могли бы начать с использования генераторов для получение мерджа этих данных с различных источников:

private function getEbooks()
{
// fetch from the DB
$stmt = $this->db->prepare(«SELECT * FROM ebook_catalog»);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);

foreach ($stmt as $data) {
yield $this->hydrateEbook($data);
}

// and from Elasticsearch (findAll uses ES scan/scroll)
$cursor = $this->esClient->findAll();

foreach ($cursor as $data) {
yield $this->hydrateEbook($data);
}
}