Домой / Аватарка / Генерация случайных строк в PHP. Что такое генераторы в PHP Сохранение памяти с помощью генераторов

Генерация случайных строк в PHP. Что такое генераторы в PHP Сохранение памяти с помощью генераторов

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

Да, генераторы определенно смотрятся хорошо, но знаете... Я не понимаю, где они могут быть полезными для меня, разве что для расчета последовательности Фибоначчи.

И они не ошибаются, ведь даже примеры в php -документации слишком упрощены. Они только объясняют, как эффективно реализовать range или итерировать по строкам файла .

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

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

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

Сначала немного контекста

Я работаю в TEA . В основном, мы разрабатываем экосистему для электронных книжек. Это покрывает весь путь от получения файлов нужного формата от издателей до размещения их на e-commerce сайте и предоставления конечному потребителю возможности читать онлайн (используя браузер, написанный @johanpoirier) или с электронной книги.

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

Итерация по крупному множеству данных

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

По традиции я должен был бы написать что-то вроде:

rulerz->satisfies($ebook, $rule)) { $filteredEbooks = $ebook; } } return $filteredEbooks; }

Проблему легко увидеть: чем больше книг, тем больше нужно памяти для $filteredEbooks.

Одно из решений - создать итератор, который бы итерировал $ebooks и возвращал подходящие. Но для этого нам нужно было бы создать новый класс, кроме того, итераторы реализируются немного утомительно... К счастью, с php 5.5.0 мы можем использовать генераторы !

rulerz->satisfies($ebook, $rule)) { yield $ebook; } } }

Да, рефакторинг метода getEbooksEligibleToWebReader для использования генератора очень прост: заменяем передачу значений в переменную $filteredEbooks конструкцией yield .

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

Бонус: RulerZ внутри использует генераторы , так что мы можем переписать метод и остаться с той же оптимизацией по выделению памяти.

rulerz->filter($ebooks, $rule); }

Агрегация нескольких источников данных

Теперь рассмотрим момент получения $ebooks. Я вам не сказал, но они по факту приходят с разных источников: реляционной БД и Elasticsearch.

Мы можем написать простой метод, агрегирующий эти два источники:

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.

Мы можем начать использовать генераторы и возвратить результат:

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); } }

Так, конечно, лучше, но у нас все равно есть проблема: наш метод getBooks выполняет слишком много работы! Мы должны разделить две ответственности (считывание данных с БД и вызов Elasticsearch ) в два метода:

getEbooksFromDatabase(); yield from $this->getEbooksFromEs(); } private function getEbooksFromDatabase() { $stmt = $this->db->prepare("SELECT * FROM ebook_catalog"); $stmt->execute(); $stmt->setFetchMode(\PDO::FETCH_ASSOC); foreach ($stmt as $data) { yield $this->hydrateEbook($data); } } private function getEbooksFromEs() { // and from Elasticsearch (findAll uses ES scan/scroll) $cursor = $this->esClient->findAll(); foreach ($cursor as $data) { yield $this->hydrateEbook($data); } }

Вы могли заметить использование yield from оператора (доступен с php 7.0), который позволяет делегировать использование генераторов . Это идеально, к примеру, для агрегации нескольких источников данных, которые используют генераторы .

yield from оператор работает с любым Traversable объектом, так что массивы и итераторы также могут быть использованы с этим оператором.

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

getEbooksFromCSV(); yield from $this->getEbooksFromDatabase(); }

Сложная ленивая (по требованию) гидрация записей БД

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

Мне нужно было импортировать сотни тысяч заказов с давней БД в нашу систему, каждый заказ содержал несколько пунктов.

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

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

Используя генераторы , мне удалось реализовать метод, который получает заказы с БД и присоединяет соответствующие пункты заказа. Все это потребляет стабильное количество памяти. Генератор избавил от надобности держать все заказы и их пункты: текущий заказ - это все, что мне нужно, чтобы сагрегировать все данные.

Имитация асинхронных задач

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

А теперь берем уже готовую функцию генерации пароля и пишем скрипт для восстановления или создания нового пароля для пользователей вашего сайта.

Скрипт восстановления пароля

Как обычно пишется скрипт?

Как всегда составляется поэтапная схема, что мы должны сделать по шагам. Все происходит в одном файле, reminder.php

1. Запускаем скрипт, только при наличии определенной переменной, например $action;

2. Для запуска процесса генерации пароля, пользователь указывает email адрес $_POST[`ema‘l`]; Для упрощения кода присвоим данное значение переменной $email.

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

4. Ищем в базе данных, в нашем случае в таблице users пользователя с таким почтовым адресом. Если нет, выдаем ошибку, что такого адреса в базе нет, и прекращаем работу скрипта.

5. Пользователь с таким адресом в базе есть, идем дальше и запускаем функцию генерации нового пароля. Также по адресу емайл получаем из базы уникальный id пользователя и пишем в переменную $id;

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

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

Как работают генераторы?

Согласно Википедии генератор “очень похож на функцию, которая возвращает массив, так как генератор имеет входные параметры, его можно вызвать и он генерирует последовательность значений”. В общих чертах генератор - это обычная функция, но вместо возврата одного значения она выдает столько значений, сколько от нее может потребоваться. Генератор выглядит как функция, но ведет себя как итератор.

В генераторах используется ключевое слово yield вместо слова return . Оно ведет себя похожим образом, и возвращает значение в точку вызова, с тем отличием, что функция не удаляется из стека, а ее состояние сохраняется в памяти. Это позволяет функции продолжить работу с предыдущего состояния при следующем ее вызове. На самом деле, вы не можете использовать ключевое слово return для возврата значения из генератора, а для того, чтобы прекратить выполнение генератора.

Руководство PHP гласит: “Когда вызывается генератор, он возвращает объект, который может быть проитерирован”. Он является объектом внутреннего класса Generator , который имплементирует интерфейс Iterator , и ведет себя, как однонаправленный итератор. Пока вы проводите итерацию над объектом, PHP вызывает генератор каждый раз, когда ему нужно получить значение. Состояние сохраняется каждый раз, как генератор выдает значение, так что в следующий раз, когда PHP затребует значение, генератор восстановит свое предыдущее состояние.

Данный код выведет следующее:

The generator has started Yielded 0 Yielded 1 Yielded 2 Yielded 3 Yielded 4 The generator has ended

Наш первый генератор

Генераторы - не новый концепт, они уже есть в таких языках, как C#, Python, JavaScript и Ruby (счетчики), их обычно можно определить по использованию ключевого слова yield . Данный код - пример на языке Python:

Давайте перепишем образец генератора Python на языке PHP. (заметьте, что оба куска кода не заботятся о проверках ошибок).

Функция-генератор открывает файл и выдает каждую линию файла, если ее затребовали. Каждый раз при вызове генератора он продолжает работу с предыдущего состояния. Работа функции не начинается сначала, так как его состояние было сохранено перед выполнением оператора yield. Как только все строки были перебраны - генератор завершает свою работу, завершается и цикл.

Возврат ключей

Итераторы в PHP состоят из пар “ключ/значение”. В нашем примере мы возвращаем только значение, а ключи в этом случае будут числовыми (ключи по-умолчанию числовые). Если вам необходимо вернуть ассоциативные пары - просто измените формат оператора yield так, чтобы он включал и ключ, используя синтаксис массивов.

Внедрение значений

Оператор yield не только возвращает значения - он может также принимать значения извне. Это делается путем вызова метода send() у объекта генератора с передачей необходимого значения в виде параметра. Это значение может быть использовано в вычислениях или других операциях. Метод передает значение в генератор как результат выполнения yield, и возобновляет выполнение.

send("stop"); } echo "{$v}n"; }

Вывод будет следующим:

Экономим память с помощью генераторов

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

Представьте результат работы функции file() , которая возвращает все строки читаемого файла в виде массива. Если сравнить результаты работы функции file() и нашей функции file_lines() над одним и тем же файлом со 100 случайными параграфами текста, то функция file() будет использовать примерно в 110 раз больше памяти, чем генератор.

Заключение

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

При вождении автомобиля – скорость это далеко не все. Но в WEB все решает скорость. Чем быстрее ваше приложение, тем лучше пользовательский опыт. Хорошо, эта статья о генераторах в PHP, так почему же мы говорим о скорости? Как вы увидите вскоре, генераторы привносят большие изменения по части скорости и потреблении памяти приложением.

Что такое PHP генераторы?

Добавленные в PHP в версии 5.5 , генераторы представляют собой функции, обеспечивающие простой механизм для циклической обработки данных, без необходимости создавать массив данных в памяти. Все еще не понимаете о чем речь? Тогда давайте посмотрим на PHP генераторы в действии.

Создаем файл generator_test.php со следующим содержанием:


$array = ;
for($i = 0; $i < $max; $i++) {
$array = $i;
}
return $array;
}

Foreach (getRange(15) as $range) {
echo "Данные {$range}
";
}

Затем в папке где у нас лежит этот файл открываем консоль и пишем следующее

Http://localhost:8000/generator_test.php

Результат будет такой:

Данные 1
Данные 2
….
Данные 15

Код выше достаточно прост. Однако, давайте сделаем небольшое изменение в нем:


echo "Данные {$range}
";
}

Теперь диапазон генерируемых чисел находится в пределах от 0 до константы PHP_INT_MAX, которая представляет собой наибольшее целое число, которое способен представить интерпретатор PHP . После этого опять идем в браузер и обновляем страницу. Однако на этот раз, вместо обычного текста получаем сообщение о том, что превышен объем доступной памяти, вследствие чего работа скрипта была аварийно завершена.

Что за досада – у PHP закончилась память! Первое что приходит на ум – это редактировать настройку memory_limit в php.ini . Но давайте спросим себя – действительно ли это так эффективно? Неужели мы хотим, чтобы какой-то единственный скрипт занимал всю доступную память?

Используем генераторы

Давайте напишем ту же самую функцию, что и выше, вызовем ее с тем же значением PHP_INT_MAX и запустим снова. Но в этот раз мы создадим функцию-генератор .

Function getRange($max = 10) {
for($i = 1; $i < $max; $i++) {
yield $i;
}
}

Foreach (getRange(PHP_INT_MAX) as $range) {
echo "Данные {$range}
";
}

Определяя функцию getRange на этот раз, мы всего лишь проходим по значениям и генерируем вывод. Ключевое слово yield похоже на инструкцию return тем, что возвращает значение из функции, но единственное отличие заключается в том, что yield возвращает значение только тогда, когда это необходимо и не пытается вместить весь массив данных в память за один раз. Перейдя к браузеру, вы должны увидеть данные, отображаемые на странице. Обратите внимание на тот факт, что генераторы в PHP могут быть использованы только лишь из функции .

Зачем нужны генераторы?

Время от времени возникают такие задачи, когда нам необходимо обработать большие объемы данных (например, файлы логов), выполнить вычисления на больших выборках из базы и т.д. И мы отнюдь не хотим, чтобы эти операции занимали всю доступную память, так мы должны стараться сохранять память насколько это возможно. Данные не обязательно должны быть большими – PHP генераторы эффективны вне зависимости от размера данных. И не забывайте, что наша цель – сделать приложение быстрым и при этом таким, чтобы оно потребляло как можно меньше памяти.

Возврат ключей

Бывают случаи, когда нам необходимо возвращать не просто значение, а пару ключ-значение. При использовании генераторов, мы можем генерировать пары ключ-значение следующим образом.

Function getRange($max = 10) {
for($i = 0; $i < $max; $i++) {
$value = $i * mt_rand();
yield $i => $value;
}
}
?>

Использовать данную функцию мы можем также как и простой массив:

Foreach (getRange(PHP_INT_MAX) as $key => $value) {
echo "Ключ {$key} имеет значение {$value}";
}

Отсылка значений генераторам

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

Function getRange($max = 10) {
for($i = 1; $i < $max; $i++) {
$inject = yield $i;
if($inject === "stop") return;
}
}

$generator = getRange(PHP_INT_MAX);

Foreach($generator as $range) {
if($range === 10000) {
// посылаем сообщение генератору
$generator -> send("stop");
}
print "Значение {$range}
";
}

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

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

Кстати, о генераторах я подробно рассказываю в моем курсе . Там есть и примеры и задания, которые помогут лучше усвоить материал.

Предлагаю вашему вниманию свое решение по генерации html на PHP. Задача вроде бы тривиальная, но хотелось бы, чтобы это было расширяемо, кратко, но в тоже время с хорошим функционалом. Получилось вроде не плохо.

Сразу скажу(как многие считают в комментариях), что задача ставилось не написать шаблонизатор (которых и так много) и не заменить шаблонизатор JavaScript. Я прекрасно знаю, что true way это разделять html и данные. Но мне понадобилось писать html в классах, для создания компонентов фреймворка, на подобие CGridView в yii, стоит ли в таких местах выносить html в отдельные файлы решать вам.

Основная цель, избавится от html в классах и функциях.

Простой пример, обычная кнопка:

CHtml::create() ->p() ->a(array("href" => "http://habrahabr.ru", "class" => "btn")) ->text("Перейти") ->render();
Результат:

Перейти

Ничего хитрого, можно было бы этим и ограничется, но захотелось циклы:
$arr = array("1" => "Первый", "2" => "Второй"); CHtml::create() ->select($options) ->each(CHtml::plainArray($arr, "value", "text")) ->option("array("value" => $data->value)") ->text("$data->text") ->end() ->endEach()
Тут понадобилось вызвать функцию plainArray() которая превращает массив в виде:
$arr = array(array("value" => "1", "text" =>"Первый"), array("value" => "2", "text" => "Второй"));
Теги внутри цикла могут содержать функции или строки с eval выражениями, вложенность любая, пример с таблицей:

$columns = array(array("id" => "NAME", "label" => "Имя"), array("id" => "AGE", "label" => "Возраст")); $data = array(array("NAME" => "Петр", "AGE" => 29), array("NAME" => "Василий", "AGE" => 32)); CHtml::create() ->table() ->thead() ->tr() ->each($columns) ->th() ->text(function($column){ return $column["label"]; }) ->end() ->endEach() ->end() ->end() ->tbody() ->each($data) ->tr() ->each($columns) ->td() ->text(function($row, $column) { return $row[$column["id"]]; }) ->end() ->endEach() ->end() ->endEach() ->render();

Незакрытые теги закрываются автоматически.

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

Class CMyHtml extends CHtml { public function a($options = array()) { $default = array("href" => "javascript:void(0)"); return parent::a(array_replace($default, $options)); } }

Class CForm { private $_lastLabel = ""; public function __construct(CModel $model, CHtml $html = null) { $this->_model = $model; $this->_html = $html ?: CHtml::create(); } public function __call($method, $ps) { $options = $ps ? $ps: array(); if ($method === "label") { $this->_lastLabel = isset($options["for"]) ? $this->_model->getLabel($options["for"]) : ""; } if ($method === "text" && $this->_lastLabel) { $options = $options ?: $this->_lastLabel; $this->_lastLabel = ""; } $this->_html->$method($options); return $this; } }

Само решение можно посмотреть и попробовать на