В моей предыдущей статье мы узнали об основах PHP Streams и о том, насколько они мощны. В этом уроке мы собираемся использовать эту силу в реальном мире. Сначала я покажу вам, как создавать свои собственные фильтры и прикреплять их к потоку, затем мы упакуем наши фильтры в приложение анализатора документов.
Рекомендуем вам прочитать предыдущую статью, если вы еще этого не сделали, так как понимание введения будет необходимо для выполнения этой части.
Полный исходный код этой статьи доступен на Github .
Использование фильтров
Как указывалось ранее, фильтры представляют собой фрагменты кода, которые можно прикрепить к потоку для выполнения операций с данными во время чтения или записи. PHP имеет хороший набор встроенных фильтров, таких как string.toupper
, string.tolower
или string.strip_tags
. Некоторые расширения PHP также предоставляют свои собственные фильтры. Например, расширение mcrypt
устанавливает mcrypt.*
И mdecrypt.*
. Мы можем использовать функцию stream_get_filters()
чтобы получить список фильтров, доступных на вашем компьютере.
Как только мы узнаем, на какие фильтры мы можем рассчитывать, мы можем добавить любое количество фильтров к ресурсу потока с помощью stream_filter_append()
:
$h = fopen('lorem.txt', 'r'); stream_filter_append($h, 'convert.base64-encode'); fpassthru($h); fclose($h);
или откройте поток, используя php://filter
:
$filter = 'convert.base64-encode'; $file = 'lorem.txt'; $h = fopen('php://filter/read=' . $filter . '/resource=' . $file,'r'); fpassthru($h); fclose($h);
В приведенных выше примерах функция fpassthru()
выведет ту же закодированную версию файла примера. Просто, не правда ли? Давайте посмотрим, что мы можем сделать с классом php_user_filter
.
Фильтрация данных по времени чтения: фильтр уценки
Наш первый пользовательский фильтр будет добавлен к потоку чтения, чтобы преобразовать данные в формате уценки из источника в разметку HTML. PHP предоставляет базовый класс php_user_filter
который мы расширяем. Этот базовый класс имеет два свойства: filtername
и params
. filtername
содержит метку, используемую для регистрации нашего фильтра в stream_filter_register()
, а params
может использоваться stream_filter_append()
для передачи данных в фильтры.
Основным рабочим методом, который мы должны переопределить, является filter()
. Этот метод вызывается родительским потоком и получает четыре параметра:
-
$in
: указатель на группу объектов сегментов, содержащих данные для фильтрации. -
$out
: указатель на другую группу сегментов для хранения преобразованных данных. -
$consumed
: счетчик, переданный по ссылке, который должен быть увеличен на длину преобразованных данных. -
$closing
: логический флаг, который имеет значениеTRUE
если мы находимся в последнем цикле, и поток собирается закрыться
Два других необязательных метода, onCreate()
и onClose()
, вызываются соответственно, когда создается и уничтожается наш класс. Они полезны, если наш фильтр должен создавать экземпляры таких ресурсов, как другие потоки или буферы данных, которые должны быть освобождены в конце преобразования.
Наш фильтр использует эти методы для обработки временного потока данных, управляемого частным свойством $bufferHandle
. Метод onCreate()
не сможет вернуть false
если поток буфера недоступен, а метод onClose()
закрывает ресурс. Наш MarkdownFilter
использует парсер Мишеля Фортина .
<?php namespace MarkdownFilter; use \Michelf\MarkdownExtra as MarkdownExtra; class MarkdownFilter extends \php_user_filter { private $bufferHandle = ''; public function filter($in, $out, &$consumed, $closing) { $data = ''; while ($bucket = stream_bucket_make_writeable($in)) { $data .= $bucket->data; $consumed += $bucket->datalen; } $buck = stream_bucket_new($this->bufferHandle, ''); if (false === $buck) { return PSFS_ERR_FATAL; } $parser = new MarkdownExtra; $html = $parser->transform($data); $buck->data = $html; stream_bucket_append($out, $buck); return PSFS_PASS_ON; } public function onCreate() { $this->bufferHandle = @fopen('php://temp', 'w+'); if (false !== $this->bufferHandle) { return true; } return false; } public function onClose() { @fclose($this->bufferHandle); } }
В методе main filter()
я собираю все содержимое в переменную $data
которая будет преобразована позже. Первый цикл циклически проходит по входному потоку, используя stream_bucket_make_writeable()
, для извлечения текущего блока данных. Содержимое каждого сегмента ( $bucket->data
) добавляется в наш контейнер, а параметр $consumed
увеличивается на длину получаемых данных ( $bucket->datalen
).
Когда все данные собраны, нам нужно создать новое пустое ведро, которое будет использоваться для передачи преобразованного содержимого в выходной поток. Мы используем stream_bucket_new()
чтобы сделать это, и если операция не удалась, мы возвращаем константу PSFS_ERR_FATAL
, которая вызовет ошибку фильтра. Поскольку нам нужен указатель ресурса для создания сегмента, мы используем свойство $bufferHandle
, которое было инициализировано ранее с помощью встроенной потоковой оболочки php://temp
.
Теперь, когда у нас есть данные и выходная корзина, мы можем создать экземпляр анализатора Markdown, преобразовать все данные и сохранить их в свойстве data
корзины. Наконец, результат добавляется к указателю ресурса $out
с помощью stream_bucket_append()
и функция возвращает константу PSFS_PASS_ON
чтобы сообщить, что данные были успешно обработаны.
Теперь мы можем использовать фильтр следующим образом:
// Require the MarkdownFilter or autoload // Register the filter stream_filter_register("markdown", "\MarkdownFilter\MarkdownFilter") or die("Failed to register filter Markdown"); // Apply the filter $content = file_get_contents( 'php://filter/read=markdown/resource=file:///path/to/somefile.md' ); // Check for success... if (false === $content) { echo "Unable to read from source\n"; exit(1); } // ...and enjoy the results echo $content, "\n";
Обратите внимание, что директива use
не имеет никакого эффекта, и при регистрации пользовательского фильтра необходимо указать полное имя класса.
Фильтрация данных во время записи: Фильтр шаблона
Как только мы конвертируем наш контент из Markdown в HTML, нам нужно упаковать его в шаблон страницы. Это может быть что угодно, от базовой структуры HTML до сложного макета страницы со стилями CSS. Таким образом, так же, как мы выполняли действие «чтение и преобразование» с входным фильтром, мы собираемся написать действие «преобразовать и сохранить», внедрив выбранный нами шаблонизатор в выходной поток. Я выбрал парсер RainTPL для этого урока, но вы можете адаптировать код к тому, который вы предпочитаете.
Структура шаблонного фильтра аналогична нашему входному фильтру. Сначала мы зарегистрируем фильтр следующим образом:
stream_filter_register("template.*", "\TemplateFilter\TemplateFilter") or die("Failed to register filter Template");
Мы используем формат filtername.*
качестве метки фильтра, чтобы мы могли использовать это *
для передачи некоторых данных нашему классу. Это необходимо, потому что, насколько я знаю, нет способа передать параметры в фильтр, примененный с использованием оболочки php://filter
. Если вы знаете способ, пожалуйста, опубликуйте его в комментариях ниже.
Затем фильтр применяется следующим образом:
$result = file_put_contents( 'php://filter/write=template.' . base64_encode('Some Document Title') . '/resource=file:///path/to/destination.html', $content );
Заголовок документа передается с использованием второй части имени фильтра и обрабатывается методом onCreate()
. В дальнейшем мы можем использовать этот прием для передачи массива сериализованных данных с пользовательскими настройками конфигурации для механизма шаблонов.
Класс TemplateFilter
:
<?php namespace TemplateFilter; use \Rain\Tpl as View; class TemplateFilter extends \php_user_filter { private $bufferHandle = ''; private $docTitle = 'Untitled'; public function filter($in, $out, &$consumed, $closing) { $data = ''; while ($bucket = stream_bucket_make_writeable($in)) { $data .= $bucket->data; $consumed += $bucket->datalen; } $buck = stream_bucket_new($this->bufferHandle, ''); if (false === $buck) { return PSFS_ERR_FATAL; } $config = array( "tpl_dir" => dirname(__FILE__) . "/templates/", "cache_dir" => sys_get_temp_dir() . "/", "auto_escape" => false ); View::configure($config); $view = new View(); if (!$closing) { $matches = array(); if (preg_match('/<h1>(.*)<\/h1>/i', $data, $matches)) { if (!empty($matches[1])) { $this->docTitle = $matches[1]; } } $view->assign('title', $this->docTitle); $view->assign('body', $data); $content = $view->draw('default', true); $buck->data = $content; } stream_bucket_append($out, $buck); return PSFS_PASS_ON; } public function onCreate() { $this->bufferHandle = @fopen('php://temp', 'w+'); if (false !== $this->bufferHandle) { $info = explode('.', $this->filtername); if (is_array($info) && !empty($info[1])) { $this->docTitle = base64_decode($info[1]); } return true; } return false; } public function onClose() { @fclose($this->bufferHandle); } }
У нас все еще есть параметр $bufferHandle
указывающий на временный поток, и у нас также есть параметр с именем $docTitle
который будет содержать (по приоритету):
- содержимое первого тега
H1
(если существует) проанализированного документа, или - декодированный контент второй части имени фильтра, или
- запасное значение по умолчанию «Без названия».
Внутри onCreate()
после инициализации потока буфера мы имеем дело со onCreate()
вариантом:
$info = explode('.', $this->filtername); if (is_array($info) && !empty($info[1])) { $this->docTitle = base64_decode($info[1]); }
Основной метод filter()
можно разделить на пять этапов. Первые два шага идентичны фильтру Markdown: все данные извлекаются из входных блоков и сохраняются в переменной $data
, затем создается пустой выходной сегмент для хранения обработанного содержимого.
На третьем этапе класс анализатора шаблонов загружается и настраивается. Я прошу систему использовать временный каталог для кэширования, отключить функцию экранирования тегов HTML и установить каталог templates
.
Используемый здесь шаблон по умолчанию очень прост, с переменными, определенными как {$VarName}
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>{$title}</title> </head> <body> {$body} </body> </html>
Четвертый шаг — это фактический анализ. Сначала я ищу заголовок документа внутри тега H1
. Затем я устанавливаю переменные body
и title
определенные в шаблоне, и, наконец, обрабатываю документ. Первый параметр метода draw()
— это имя шаблона, второй говорит, что нужно возвращать строку вместо ее печати.
Последний шаг — поместить проанализированный контент в выходной сегмент и добавить его в выходной ресурс, возвращая PSFS_PASS_ON
.
Собираем все вместе: анализатор документов
Теперь, когда у нас есть основные блоки, пришло время создать нашу утилиту синтаксического анализа документов. Утилита приложение живет в своем собственном каталоге mddoc
. Наши пользовательские фильтры живут в каталоге lib
используя каталог PSR-0
и структуру пространства имен. Я использовал Composer для отслеживания зависимостей
"require": { "php": ">=5.3.0", "michelf/php-markdown": "*", "rain/raintpl": "3.*" },
и автозагрузка:
"autoload": { "psr-0": { "MarkdownFilter": "lib", "TemplateFilter": "lib" } }
Основной файл приложения — это mddoc
который можно выполнить так:
$ /path/to/mddoc -i /path/to/sourcedir -o /path/to/destdir
Файл приложения выглядит так:
#!/usr/bin/env php <?php /** * Markdown Tree Converter * * Recursive converts all markdown files from a source directory to HTML * and places them in the destination directory recreating the structure * of the source and applying a template parser. */ // Composer autoloader require_once dirname(__FILE__) . '/vendor/autoload.php'; // Deals with command-line input arguments function usage() { printf( "Usage: %s -i %s -o %s\n", basename(__FILE__), '/path/to/sourcedir', '/path/to/destdir' ); } if (5 > $argc) { usage(); exit; } $in = array_search('-i', $argv); $src = realpath($argv[$in+1]); if (!is_dir($src) || !is_readable($src)) { echo "[ERROR] Invalild source directory.\n"; usage(); exit(1); } $out = array_search('-o', $argv); $dest = realpath($argv[$out+1]); if (!is_dir($dest) || !is_writeable($dest)) { echo "[ERROR] Invalild destination directory.\n"; usage(); exit(1); } // Register custom read-time MarkdownFilter stream_filter_register("markdown", "\MarkdownFilter\MarkdownFilter") or die("Failed to register filter Markdown"); // Register custom write-time TemplateFilter stream_filter_register("template.*", "\TemplateFilter\TemplateFilter") or die("Failed to register filter Template"); // Load directory iterator for source $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($src), RecursiveIteratorIterator::SELF_FIRST ); // For every valid item while ($it->valid()) { // Exclude dot items (., ..) if (!$it->isDot()) { // If current item is a directory, the same empty directory // is created on destination if ($it->isDir()) { $path = $dest . '/' . $it->getFileName(); if ((!@is_dir($path)) && !@mkdir($path, 0777, true)) { echo "Unable to create folder {$path}\n"; exit(1); } } // If current item is a markdown (*.md) file it's processed and // saved at the coresponding destination path if ($it->isFile() && 'md' == $it->getExtension()) { $path = $it->key(); if (!is_readable($path)) { echo "Unable to read file {$path}\n"; exit(2); } $content = file_get_contents( 'php://filter/read=markdown/resource=file://' . $path ); if (false === $content) { echo "Unable to read from source '" . $path . "'\n"; exit(3); } $pathinfo = pathinfo($dest . '/' . $it->getSubPathName()); $target = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.html'; $result = file_put_contents( 'php://filter/write=template.' . base64_encode(basename($path)) . '/resource=file://' . $target, $content ); if (false === $result) { echo "Unable to write file '" . $target . "'\n"; exit(4); } } } $it->next(); } exit(0);
Сначала мы включаем наш автозагрузчик, затем продолжаем проверку аргументов:
- командная строка должна соответствовать приведенному выше примеру,
- исходный каталог должен существовать и быть читаемым,
- каталог назначения должен существовать и быть доступным для записи.
Затем мы регистрируем пользовательские фильтры (с полным путем к классу, запоминаем) и создаем экземпляр объекта RecursiveIteratorIterator
для RecursiveIteratorIterator
исходного каталога. Основной цикл перебирает все допустимые элементы, выбранные итератором. Все элементы, кроме файлов точек, обрабатываются следующим образом:
- если текущий элемент является каталогом, попробуйте заново создать относительный путь с тем же именем, начиная с пути назначения.
- если текущий элемент является файлом разметки ( .md), содержимое файла считывается в переменную с использованием фильтра считывания
markdown
, тогда новый файл с расширением.html
записывается по тому же относительному пути, начиная с целевого каталога с шаблон. `фильтр применен.
В результате структура вашей документации будет полностью преобразована в HTML с помощью одной команды. Неплохо.
Резюме
Здесь мы рассмотрели много полезных вопросов, и у нас также есть полнофункциональная утилита, чтобы… добавить в нашу цепочку инструментов. Я оставлю это на ваше усмотрение, чтобы продвинуться дальше и создать другие инструменты и компоненты, подобные этим, для улучшения наших проектов. Удачного кодирования!