Создание простого шаблонизатора на PHP - часть 2

В предыдущей статье мы создали структуру проекта и подготовили метод Engine::render() к обработке файла представления и переданных в него переменных. Даже сейчас это уже более эффективно, чем использовать чистый PHP для отображения страниц.

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

Стандартизация пути к представлению

Нужно сделать несколько вещей с путём к представлению. В основном, нам нужно учесть абсолютные пути в Windows.

Убедимся, что это относительный путь, а не абсолютный. Если бы мы попробовали создать URL vfsStream в Windows-формате вроде C:xampphtdocs..., то получили бы следующий поток для обработки vfs://c:xampphtdocs... Он бы не работал. Дело даже не в обратных слэшах, а во втором двоеточии, которое приведёт к ошибке. Поэтому относительный путь будет лучше.

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

Затем разделим адрес на путь (например, views или views/pages) и имя файла home.php.

Начнём с очистки пути, чтобы можно было создать из него URL вида vfsStream. Сначала заменим все обратные слэши. Также нужно убедиться, что путь на данном этапе абсолютный:

Файл: src/Engine.php

public function render()
 {
     if (!empty($params)) extract($params);
     if (file_exists($view)) {
         $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
         ob_start();
         include $view;
         ob_get_flush();
     } else {
         throw new Exception(sprintf('Файл %s не найден.', $view));
     }
 }

Мы используем realpath($view), чтобы убедиться, что, даже если передали относительный путь, такой как view/home.php, то переменная $view будет содержать абсолютный путь.

Наш первый шаблон Regex

Теперь нужно удалить все ненужные части из начала абсолютного адреса. В зависимости от операционной системы мы должны получить адрес формата

C:/xampp/htdocs/sites/template/views/home.php

или

/var/www/template/view/home.php.

В Windows нужно убрать C:/, а для Linux/Unix - первый слэш /. Для этого можно использовать preg_replace, чтобы обработать сразу оба случая:

Файл: src/Engine.php

public function render($view, $params = [])    
 {
     if (!empty($params)) extract($params);
     if (file_exists($view)) {
         $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
         $vfs_viewpath = preg_replace('~^([w.-]:)?/+(.*)$~', '$2', $view);
         ob_start();
         include $view;
         ob_get_flush();
     } else {
         throw new Exception(sprintf('Файл %s не найден.', $view));
     }
 }

Рассмотрим, что делает preg_replace. Мы используем символ тильды ~ в качестве ограничителя. Таким образом можно использовать слэш в шаблоне без необходимости экранировать его. Ведь мы хотим найти первый слэш и всё, что расположено перед ним.

Первая часть регулярного выражения ^([w.-]+:)? совпадает со всем, что может быть обработчиком потока или диском Windows. Символ ^ говорит, что совпадение обязано быть в самом начале строки. Затем следует «группа». Всё, что в скобках, становится группой, которая хранится в пронумерованной переменной для дальнейшего использования в шаблоне или для замены. Она будет храниться в переменной $1. Если бы использовали preg_match('~^([w.-]+:)?/(.*)$~', $view, $match), то могли бы получить это значение в $match[1].

Внутри группы есть выражение [w.-]+:. Оно совпадает с любыми буквенными символами, знаками подчёркивания, точками и дефисами, за которыми следует двоеточие. В общем случае оно совпадёт с C:, E:, file:, some.custom-stream: или another_stream:. При сопоставлении дефиса также важно, чтобы он шёл последним в квадратных скобках, иначе он будет расценен как диапазон (например, [a-z] совпадает с любой буквой от a до z).

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

Знак вопроса (?) после группы означает, что группа ([w.-]+:) необязательная. Она может быть проигнорирована или найдена только один раз. Поэтому, если путь не начинается с этого шаблона, его остаток всё равно будет являться совпадением.

Далее идет прямой слэш /. Это совпадёт с абсолютными путями в стиле Unix, такими как /var/www/template/views/home.php.

Вторая группа регулярного выражения используется для нашего пути vfs (.*). Она ищет совпадения любых символов любое количество раз.

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

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

Когда шаблон находит совпадение, мы используем $2, чтобы произвести замену того, что совпало со второй группой (.*). Это всё, кроме начальных C:/ или /.

Создание потока vfsStream и файла vfs

Далее надо создать поток vfsStream на основе очищенного пути к представлению.

public function render($view, $params = [])    
 {
     if (!empty($params)) extract($params);
     if (file_exists($view)) {
         $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
         $vfs_viewpath = preg_replace('~^([w.-]:)?/+(.*)$~', '$2', $view);
         vfsStream::create($vfs_viewpath);
         $vfs_file = vfsStream::url($vfs_viewpath . '.php');
         ob_start();
         include $view;
         ob_get_flush();
     } else {
         throw new Exception(sprintf('Файл %s не найден.', $view));
     }
 }

Мы добавляем '.php' в конец vfsStream::url потому, что файлы, которые использует шаблонизатор php, могут иметь расширения, отличные от .php. Но итоговый файл включения всё равно будет файлом PHP.

Теперь у нас есть файловый поток по виртуальному пути vfs://projects/template/views/home.php.php (в моём случае). Но его можно рассматривать как настоящий файл и применять к нему функции для работы с файлами или потоками. Мы также можем включать его как любой другой файл.

Чтобы создать файл, нужно заполнить его. Затем включить его в буфер вывода.

public function render($view, $params = [])    
 {
     if (!empty($params)) extract($params);
     if (file_exists($view)) {
         $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
         $vfs_viewpath = preg_replace('~^([w.-]:)?/+(.*)$~', '$2', $view);
         vfsStream::create($vfs_viewpath);
         $vfs_view = vfsStream::url($vfs_viewpath . '.php');
         file_put_contents($vfs_view, file_get_contents($view));
         ob_start();
         include $vfs_view;
         ob_get_flush();
     } else {
         throw new Exception(sprintf('Файл %s не найден.', $view));
     }
 }

Теперь у нас есть отдельный файловый поток, который можно изменять независимо от исходного файла представления. Это позволит заменить наш собственный синтаксис в исходной переменной $view и вставлять чистый PHP-код в переменную $vfs_view.