Шаблонизатор Miranda - выход в свет. Часть 1

PHP Шаблонизатор MirandaХочу представить вам мой небольшой проект, шаблонизатор под названием Miranda. Начну с самого начало. Где-то года два назад я встретил статью одного программиста, он описывал работу над своим собственным шаблонизатором, в то время я работал с Smarty. Мне было интересно следить за развитием его проекта, но вскоре у меня отнялась всякое желание делать проекты пользуясь шаблонизаторами. Переписать проекты которые уже есть на Smarty мне не хотелось, решил поддерживать их как есть, до какого-то времени. Недавно, мой интерес к шаблонизаторам вернулся, причиной тому стала восхищение собственного шаблонизатора в молодом фреймворке Laravel.

В нем есть все, что мне нужно и я активно перешел на пользования шаблонизатора которого предостовляет мне фреймворк laravel. Недавно мне понадобилась написать простенький сайт, но без использования фреймворка работая в паре с дизайнером который не понимает PHP. Он его знает, но я не доверяю вставку в своем сайте чужого php кода. Вот и пришла идея написать шаблонизатор, как основу и вдохновение я взял принципы шаблонизатора Laravel. Сразу хочу сказать, что код от туда не выдергивал (можете проверить :) ) мне понравился принцип работы, а уже сам код я в удовольствие напишу и сам.

Я не прикручивал к нему всякие модули кэширования или защит от xss-атак, т.к. шаблонизатор не должен этим заниматься. У него одна цель - разделять логику от представления. Вы возможно скажете, что защита от xss необходимость в шаблонизаторе, т.к. на данные из шаблона и нацелена данная атака. Я отвечу, что о валидации данных которые попадут дальше в тело скрипта нужно было думать еще до написания первого $_POST, $_GET в своем скрипте.

Кэширование в шаблонизаторе это тоже бесполезно. Например, вы хотите получить инструмент для обрезки листа бумаги, но в замен вам дадут ножницы по стали. В результате вы выполните вашу задачу, но потратите больше нагрузки в манипуляции этим инструментом. Если уже писать библиотеку которая подразумевает выполнение одной задачи, то эту задачу она и должны выполнить. Вы же не станете интуитивно искать проблемы в работе скрипта кэширования в библиотеке TemplateEngine.php первым делом будете искать ключевое слово связанное с cache.

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

Философия в сторону, начнем осмотр шаблонизатора

Актуальную версии вы можете скачать тут.

<?php
/* ---------------------------
 * Легкий шаблонизатор Miranda
 * Автор: Mowshon
 * Сайт автора: mowshon.ru
 * ---------------------------
 */
class Miranda {
    
    public $TemplateFiles = '';
    public $CompiledFiles = '';
    public $Extensions = array('php', 'tpl');
    public $SpecialExtension = 'tpl';
    public $separator = DIRECTORY_SEPARATOR;
    public $TemplateVariables = array();
    public $Sections = array();
    
    public function __construct() {
        // Полный путь до папки с файлами шаблона
        if(!$this->TemplateFiles)
            $this->TemplateFiles = dirname( dirname( __FILE__ ) ) . $this->separator . 'templates' . $this->separator;
        // Путь до папки с копилиными файлами
        if(!$this->CompiledFiles)
            $this->CompiledFiles = $this->TemplateFiles . 'compiled' . $this->separator;
    }
    
    public function __destruct() {
        $this->TemplateVariables = array();
        $this->Sections = array();
    }
    
    public function __set($name, $value) {
        $this->TemplateVariables[$name] = $value;
    }
    
    public function TemplatePath( $path ) {
        // Если вам нужно хранить данные шаблона по определенному пути, тогда 
        // до вызова в скрипте метода make, выполните вызов метода TemplatePath.
        // указывая в нем полный путь к папке
        // $Miranda->TemplatePath('/var/www/templates/');
        $this->TemplateFiles = $path;
    }
    
    public function make( $expresion, $TemplateVariables = array() ) {
        // Конвертируем искусственный путь до файла в полный путь
        $path = $this->ConvertPath( $expresion );
        if(!$path) {return 'File does not exist';}
        // Если метод make был вызван с массивом содержащий названия
        // и значения будующих переменных которые будет видны только в файлах шаблона
        // добавляем их к ранее действующим
        $this->UnionVariables( $TemplateVariables );
        
        // Проверяем если файл шаблона является особым шаблонным файлом
        // т.е. с расширением .tpl
        if(!$this->isSpecialExtension( $path )) {
            // Файл является PHP файлом, вызываем его как есть
            return $this->open( $path );
        }
         else {
             // Файл является особым шаблонным, отправляем его искусственному компилятору
             $CompiledFilename = $this->Compile( $expresion );
             // Метод Compile вернет файл из папки template / compiled с расширением PHP
             // Открываем его как обычного PHP файла
             return $this->open( $CompiledFilename );
         }
    }
    
    public function attach( $path ) {
        // Подключения файлов внутри шаблона
        $this->make( $path, $this->TemplateVariables );
    }
    
    public function inject($section, $value) {
        // Создание блоков(секции) вне шаблонного файла, т.е. до вызова make
        $this->Sections[$section] = $value;
    }
    
    public function section( $Content ) {
        // Открывает содержимое блока(секции)
        $RequireSections = $this->RequireSections( $Content );
        if(count($RequireSections)) {
            return strtr($Content, $RequireSections);
        }
         else {
             return $Content;
         }
    }
    
    private function UnionVariables( $Variables ) {
        // Объединение переменных в глобальном массиве
        $this->TemplateVariables = array_merge($this->TemplateVariables, $Variables );
        return True;
    }
    
    private function open( $path ) {
        // Открывает PHP файл содовая переменные область видимости которых
        // будет только в этом файле или в файлах которых он подключает
        if( count( $this->TemplateVariables ) ) {
            foreach( $this->TemplateVariables as $key => $val ) {
                ${$key} = $val;
            }
        }
        require_once( $path );
    }
    
    private function ConvertPath( $path ) {
        // Конвертирует искусственную аббревиатуру файла в полный путь до файла шаблона
        // пример: templ.file => /path/to/templates/templ/file.php или .tpl зависит от приоритета
        // т.е. какое расширение первое в массиве $this->Extensions
        $InitialPath = explode('.', $path);
        foreach($this->Extensions as $extension) {
            $File = $this->TemplateFiles . implode($this->separator, $InitialPath) . '.' . $extension;
            if(file_exists($File)) {
                return $File;
            }
        }
        return False;
    }
    
    private function extension( $file ) {
        $File = explode('.', $file);
        return $File[ count($File) - 1 ];
    }
    
    private function isSpecialExtension( $file ) {
        // Проверка если запрашиваемый файл из $file имеет особое разрешение (.tpl)
        return( $this->extension($file) == $this->SpecialExtension )? True : False;
    }
    
    /*
     * Искусственный компилятор TPL файлов в PHP
     */
    
    private function Compile( $expresion ) {
        // Подготавливаем древо подключающихся файлов шаблона
        $ParserLayoutTree = $this->ParserLayoutTree( $expresion );
        // Будущее (или уже существующее) название файла в папке templates / compiled
        $CompiledFilename = $this->CompiledFilename( $ParserLayoutTree );
        
        // Если файлы участвующие в древе $ParserLayoutTree потерпели изменения
        // с момента последней компиляции, выполняем компиляцию повторно
        $WasEdited = $this->WasEdited($CompiledFilename, $ParserLayoutTree);
        if( file_exists( $CompiledFilename ) and $WasEdited ) {
            // Файл существует, изменения не присутствуют
            return $CompiledFilename;
        }
         else {
             // Объединяем подключающие друг друга файлы вместе с выполнением замен
             // содержимого в блоках(секциях)
             $SourceOfUnionLayout = $this->UnionTreeContent( $ParserLayoutTree );
             // Добавляем дату последнего изменения исходного TPL файла
             // или суммарную дату всех файлов которые входят в древо подключения ($ParserLayoutTree)
             $Content = $this->addFileLastEdit( $ParserLayoutTree );
             // Конвертируем TPL функции в PHP
             $Content .= $this->ConvertToCode( $SourceOfUnionLayout );
             $SaveAsCompiledFile = $this->SaveAsCompiledFile($CompiledFilename, $Content);
             return $CompiledFilename;
         }
    }
    
    private function OpenFile( $filename ) {
        return file_get_contents($filename);
    }
    
    private function CompiledFilename( $LayoutTree ) {
        $LayoutTreeFilename = $this->LayoutTreeFilename( $LayoutTree );
        return $this->CompiledFiles . $LayoutTreeFilename . '.php';
    }
    
    private function ConvertToCode( $Content ) {
        // Конвертируем специальные искусственные функции шаблонизатора
        // в их PHP альтернативы
        $Content = $this->ConvertTags($Content);
        $Content = $this->ConvertConditions($Content);
        return $Content;
    }
    
    private function ConvertTags( $Content ) {
        // Конвертирование искусственных перемен шаблонизатора
        preg_match_all("#{{(.+?)}}#si", $Content, $matches);
        if(count($matches[0])) {
            foreach($matches[0] as $match) {
                preg_match_all("#{{(.+?)}}#si", $match, $matches);
                if(count($matches[1])) {
                    $Valiable = trim($matches[1][0]);
                    // Если в тело блоков {{}} использован знак присвоения
                    // значения "=" значит не афишируем значение переменной
                    if(preg_match("#=#", $Valiable)) {
                        $Content = str_replace($match, "<?php {$Valiable}; ?>", $Content);
                    }
                    else {
                        // Блок {{}} не содержит знака присвоения, афишируем ее содержимое
                        $Content = str_replace($match, "<?php echo {$Valiable}; ?>", $Content);
                    }
                }
            }
        }
        return $Content;
    }
    
    private function ConvertConditions( $Content ) {
        // Конвертируем функции шаблонизатора в альтернативные PHP
        $Content = preg_replace("#@\s?begin\s?elseif\s?\((.*?)\)#si", "<?php } elseif($1) { ?>", $Content);
        $Content = preg_replace("#@\s?begin\s?(.+?)\s?\((.*?)\)#si", "<?php $1($2) { ?>", $Content);
        $Content = preg_replace("#@\s?include\s?\((.*?)\)#si", '<?php $this->attach('."'$1'".'); ?>', $Content);
        $Content = str_replace("@else", "<?php } else { ?>", $Content);
        $Content = str_replace("@end", "<?php } ?>", $Content);
        return $Content;
    }
    
    private function SaveAsCompiledFile( $filename, $content ) {
        $Create = fopen($filename, 'w+');
        return fwrite($Create, $content);
    }
    
    private function addFileLastEdit( $LayoutTree ) {
        $lastedit = $this->LastEditSum($LayoutTree);
        return "<!--?php # lastedit[{$lastedit}] ?-->\n";
    }
    
    private function WasEdited($CompiledFilePath, $LayoutTree) {
        if(!file_exists($CompiledFilePath)) {return False;}
        $CopiledFileLastedit = $this->SavedLastEditDateInCompiledFile($CompiledFilePath);
        $SourceFilesLastedit = $this->LastEditSum($LayoutTree);
        return($CopiledFileLastedit != $SourceFilesLastedit)? False : True;
    }
    
    private function LastEditSum($LayoutTree) {
        $lastedit = 0;
        foreach($LayoutTree as $layout) {
            $lastedit += filemtime( $this->ConvertPath( $layout ) );
        }
        return $lastedit;
    }
    
    private function SavedLastEditDateInCompiledFile( $CompiledFilePath ) {
        $CompiledContent = $this->OpenFile($CompiledFilePath);
        preg_match("#lastedit\[([0-9]+)\]#", $CompiledContent, $match);
        return $match[1];
    }
    
    private function ParserLayoutTree( $expresion ) {
        // Создание древа подключающихся шаблонных файлов
        $LayoutTree[] = $expresion;
        $tpl_file = $this->ConvertPath( $expresion );
        while(True) {
            $OpenLayout = $this->OpenFile( $tpl_file );
            // Если в содержимое файла, есть открытие блоки(секции) добавляем их
            // содержимое в глобальный массив хранения блоков(секции)
            $this->CreateSections( $OpenLayout );
            // Проверяем если данный файл не является частью другого
            // т.е. если нет сверху вызов родительного шаблона @layout(main)
            $FindLayout = $this->FindLayout( $OpenLayout );
            if($FindLayout) {
                $LayoutTree[] = trim($FindLayout);
                $tpl_file = $this->ConvertPath($FindLayout);
            }
             else {
                 break;
             }
        }
        
        return $LayoutTree;
    }
    
    private function FindLayout( $Content ) {
        // Поиск в содержимое файла запрос на вывод данных в родительский файл
        preg_match("#@\s?layout\s?\((.+?)\)#", $Content, $matches);
        return @$matches[1];
    }
    
    private function LayoutTreeFilename( $Tree ) {
        return implode('_', $Tree);
    }
    
    private function CreateSections( $Content ) {
        preg_match_all("#@\s?section\s?\((.+?)\)(.+?)@\s?section_end#siu", $Content, $matches, PREG_SET_ORDER);
        foreach($matches as $value) {
            $this->Sections[trim($value[1])] = $value[2];
        }
    }
    
    private function UnionTreeContent( $LayoutTree=array() ) {
        $MainLayoutInTree = $LayoutTree[count($LayoutTree)-1];
        $this->FillSectionsWithContent();
        $MainLayout = $this->OpenFile( $this->ConvertPath( $MainLayoutInTree ) );
        return $this->section( $MainLayout );
    }
    
    private function FillSectionsWithContent() {
        if(count($this->Sections)) {
            foreach($this->Sections as $key=>$value) {
                $this->Sections[$key] = $this->section( $value );
            }
        }
    }
    
    private function RequireSections($Content) {
        $SectionsToSwitch = array();
        preg_match_all("#@\s?view_section\s?\((.+?)\)#siu", $Content, $matches, PREG_SET_ORDER);
        foreach($matches as $value) {
            $SectionsToSwitch[trim($value[0])] = @$this->Sections[trim($value[1])];
        }
        return $SectionsToSwitch;
    }
    
}
?>

Особенности шаблонизатора

  1. Поддержка php файлов шаблона, так и специальное расширение .tpl которая означает, что исходный код который содержит искусственный синтаксис tpl придется компилировать в PHP.
  2. Компиляция только один раз, в последующие разы будет вызываться уже компилированный файл.
  3. Легкий синтаксис. Я не стал придумывать новый синтаксис, ставить свои стандарты как это сделано в Smarty.
  4. Поддерживает создание блоков
  5. Иерархия вызова файлов подключающиеся друг друга в обратном порядке файлы шаблона.
  6. Динамика в вызове посторонних функции или выполнения разного рода циклов.

Установка шаблонизатора

  1. Скачиваем свежую версию тут.
  2. Разархивируем файлы в нужном вам месте (библиотека не имеет строгие правила насчет установки места где будет шаблоны, всегда можно указать полный путь вызывая метод TemplatePath)
  3. В папке templates должны быть папка compiled с правами 777

Если папка с шаблонами будет расположена не на том же уровне, что и папка libs выполните следующий код во время вызова класса.

<?php
include_once('libs/Miranda.php');
$Miranda = new Miranda;
$Miranda->TemplatePath('/var/www/www/templates/');
?>
Чтобы не указывать путь к папке, можно сразу прописать путь в начале класса, в переменную $TemplateFiles.

Примеры работы

Посмотрим несколько моментов в работе шаблонизатора, по вырастающей сложности и динамики.

Переменные - в шаблон переменные можно добавить двумя способами. Указывая их до вызова метода make или указывать массив как аргумент метода make. Пример будет выполнен  используя обычный php файл как файл шаблона. Про .tpl файлы немного позже.

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

<?php
include_once('libs/Miranda.php');
$Miranda = new Miranda;
$Miranda->template_engine = 'Miranda';
echo $Miranda->make('main', array(
    'author' => 'mowshon',
    'os' => 'ubuntu'
));
?>
В make можно не указывать расширение к файлам, приоритетным расширением всегда будет .php . Если будет два файла main.php и main.tpl подключатся будет main.php. В примере выше, мы создали три переменные которые доступны для вывода в шаблоне, это: $template_engine, $author и $os.

Шаблонный файл из template / main.php

Шаблон вызван через шаблонизатор: <?=$template_engine?><br />
Автор: <?php echo $author ?>, ОСь: <?php echo $os ?>
Указывать в шаблоне разные переменные как $this->var и т.д. не нужно. Можно просто вызвать переменную по ее названию.

Шаблонизатор Miranda Часть 1 | Часть 2 | Часть 3

27 июля 2012, 22:19 PHPmowshon3787RSS
Оставьте комментарий!

Комментарий будет опубликован после проверки

Имя и сайт используются только при регистрации

(обязательно)