После статьи Как написать свой фреймворк на php, один знакомый программист задал мне такой вопрос: а как работает шаблонизатор в Yii, а конкретно функция render("filename", $variables = array())?
Если углубиться в историю php (а он был написан как шаблонизатор в языке perl), то будет ясно, лучшего шаблонизатора, чем сам язык на нем не написать. Smarty и т.п. библиотеки, ограничены своим API. И работа с ними напоминает история про Active Record, которую я описал в предыдущей статье.
Итак, как можно написать простейший шаблонизатор, с тем же функционалом, что и в Yii.
Однажды, на страницах этого блога, уже была статья, про такой шаблонизатор. Однако, там был обзор готовой библиотеки. Сейчас, я расскажу подробнее о том, как оно работает.
Создадим класс, и сразу опишем методы, которыми он будет владеть.
class tpl{ static function render($filename,$variables=array(),$output=true){} static function renderPartial($filename,$variables=array(),$output=true){} }
с этими методами знаком программист работающий с Yii. Метод render выводит всю страницу, включая в специально отведенное место содержимое файла $filename. Метод renderPartial просто выводит содержимое файла $filename на экран. По своей сути, метод render это последовательный renderPartial($filename) и renderPartial('index'). Следовательно, для работы шаблонизатора, нам потребуется описать этот метод.
Основа его работы простое подключение требуемого файла методом include, а чтобы переменные были доступны в нем самостоятельно, а не как части массива, используется подзабытая функция extract
static public function renderPartial($filename,$variables=array(),$output=true){ extract($variables); if( file_exists(ROOT.'tpls/'.str_replace('..','',$filename).'.php') ){ if( !$output ) ob_start(); include ROOT.'tpls/'.$filename.'.php' return !$output?ob_get_clean():true; } else throw new Exception('File '. ROOT.'tpls/'.$filename.'.php not found'); }
Чтобы загнать содержимое файла в переменную, используется известный прием с ob_start() ob_get_clean(). Это такие функции, которые загоняют стандартный поток вывода в другие рельсы. Говоря другим языком, они буферизируют вывод. Подобно тому, как в windows данные помещаются в буфер обмена. Подробнее про это можно почитать здесь
т.е. если параметр
$output
оставить без изменений, то они и не задействуются. Но если его установить в false, то все, что выведет файл $filename.php не пойдет в основной поток, а будет собрано и возвращено методом renderPartial. Не правда ли, очень удобно!?
Вот и вся магия. Теперь опишем метод render
static public function render($filename,$variables=array(),$output=true){ $content = self::renderPartial($filename,$variables,false); return self::renderPartial('main',array_merge(array('content'=>$content),$variables),$output); }
Как и писал выше, дважды вызываем метод renderPartial. Сперва с output==false, для целевого файла. Второй раз вызываем с тем же, что был подан в сам render. В файле main.php, содержимое файла $filename.php будет доступно в переменной $content.
Это не самое веселое, что можно сделать. Что, если в переменную $content загонять экземпляр класса, с созданным методом __toString. Можно будет сделать рендер в нормальном порядке. Сперва main а затем $filename в нем. Так как и должно быть.
Но оставлю это для вашей фантазии.
В рамках статей о написании идеального велосипеда фреймворка добавим в класс контроллера, эти два метода. Отличать их будет только параметр static, перед названием метода. Он нам больше не понадобится. Так как для каждого контроллера будет создан свой экземпляр.
<?php class Controller extends Singleton{ function __call( $methodName,$args=array() ){ if( is_callable( array($this,$methodName) ) ) return call_user_func_array(array($this,$methodName),$args); else throw new Except('In controller '.get_called_class().' method '.$methodName.' not found!'); } public $tplPath = ''; public $tplControllerPath = ''; function __construct(){ $this->tplPath = APP.'views/'; $this->tplControllerPath = APP.'views/'.strtolower(str_replace('Controller','',get_called_class())).'/'; } private function _renderPartial($fullpath,$variables=array(),$output=true){ extract($variables); if( file_exists($fullpath) ){ if( !$output ) ob_start(); include $fullpath; return !$output?ob_get_clean():true; }else throw new Except('File '.$fullpath.'.php not found'); } /** * renderPartial - метод доступный в контроллере, для вывода файла шаблона. * Не запускает больше никаких файлов. Удобен при ajax вызове контроллера * * @params $filename - название шаблона в папке views/название контроллера/{}.php * @params $variables - ключи массива будут доступны в шаблоне как переменные с * теми же именами * @params $output - если указать false, то данные из шаблона не будут выведены в основной поток а будут возвращены методом */ public function renderPartial($filename,$variables=array(),$output=true){ $file = $this->tplControllerPath.str_replace('..','',$filename).'.php'; return $this->_renderPartial($file,$variables,$output); } /** * render - метод выполняет полный вывод страницы на экран. При этом в нее включается * содержимое файла шаблона $filename * * @params - все параметры идентичны renderPartial */ public function render($filename,$variables=array(),$output=true){ $content = $this->renderPartial($filename,$variables,false); return $this->_renderPartial($this->tplPath.'main.php',array_merge(array('content'=>$content),$variables),$output); } }
Вы все еще не форкнули этот фреймворк? Чего же вы ждете? Давайте сделаем идеальный фреймворк вместе.
Комментарии
Я как раз искал что-то подобное, спасибо вам огромное за этот труд!
Вот только я не совсем понял, зачем нужны:
ob_start(); и ob_get_clean();
Спасибо, за внимание. = )
Если передать true будет ровно тоже самое.
Я понимаю в принципе как работают функции буфера, но понять не могу, зачем они здесь, так как в независимости от 3 переменной, будет один и тот же результат.
Или я чего то не понимаю ))
или так
ob_get_clean отчищает вывод после ob_start
Спасибо вам, что объяснили!