IDeal
В предыдущих постах мы разработали минимальную структуру фреймворка. В этой статье, наведем немного лоска - добавим простенькую тему с bootstrap'ом, меню и подробнее рассмотрим работу шаблонизатора и роутера (маршрутизатора сайта).
Роутер
В прошлой статье мы создали роутер, который используя логику регулярных выражений распознает входящую sef ссылку и направляет на нужный контроллер и его метод.
Т.е. из такой строки
/user/profile/12
роутер делает массив
array('controller'=>'user','action'=>'profile', 'id'=>15)
Мы не стали в прошлый раз писать пример использования такого решения. Исправим ситуацию, но перед этим доработаем роутер.
Перенесем массив правил роутера в конфигурационный файл фреймворка. Этот массив будут заполнять разработчики, которые будут писать сайты на нашем фреймворке. Поэтому он должен быть максимально доступен.
... 'router' => array( '([a-z0-9+_\-]+)/([a-z0-9+_\-]+)/([0-9]+)' => '$controller/$action/$id', '([a-z0-9+_\-]+)/([a-z0-9+_\-]+)' => '$controller/$action', '([a-z0-9+_\-]+)(/)?' => '$controller', ), ...
Кроме того вы можете видеть, что мы изменили правую часть массива.
Внесем изменения в код роутера, а потом разберем его работу.
function parse($path){ $request = $_REQUEST; $request['controller'] = app::gi()->config->default_controller; $request['action'] = app::gi()->config->default_action; $request['id'] = 0; $parts = parse_url($path); if (isset($parts['query']) and !empty($parts['query'])) { $path = str_replace('?'.$parts['query'], '', $path); parse_str($parts['query'], $req); $request = array_merge($request, $req); } foreach(app::gi()->config->router as $rule=>$keypath) { if (preg_match('#'.$rule.'#sui', $path, $list)) { for ($i=1; $i<count($list); $i=$i+1) { $keypath = preg_replace('#\$[a-z0-9]+#', $list[$i], $keypath, 1); } $keypath = explode('/', $keypath); foreach($keypath as $i=>$key) { $request[$this->path_elements[$i]] = $key; } } } return $request; }
Во первых мы заполняем массив выходных данных значениями по умолчантю, которые берем из нашего config.php. Затем мы парсим входной $path на наличие get переменных. Если они есть, то их просто нужно добавить к массиву выходных данных. parse_url - разделяет url на составляющие. Одной из таких составляющих является query. Это все что в URL идет после знака вопроса
Пример
print_r(parse_url('http://xdan.ru/user/profile/15?login=1111&password=1222#12'));
Результат
Array( [scheme] => http [host] => xdan.ru [path] => /user/profile/15 [query] => login=1111&password=1222 [fragment] => 12 )
Эту строку key=value параметров в свою очередь парсит функция parse_str
Пример
parse_str('login=1111&password=1222', $output); print_r($output);
Результат
Array( [login] => 1111 [password] => 1222 )
C query частью строки разобрались, теперь используя правила регулярных выражений, выделим из пути необходимые данные.
На 12 строчке мы видим перебор всех правил из конфига. На 13 проверяем - подходит ли очередное правило. Если да, то берем массив результатов и поочередно заменяем его элементами - правую часть правила.
Пример
Сработало правило '([a-z0-9+_\-]+)/([a-z0-9+_\-]+)/([0-9]+)' => '$controller/$action/$id',
Тогда массивом результатов применения его левой части к входной строке /user/profile/15
будет массив
array( '/user/profile/15', 'user', 'profile', '15' )
Перебирая этот массив, начиная со второго элемента, заменяем в правой части правила $controller/$action/$id
ключи $([a-z]+)
Т.е. на строке 14, при каждой замене, правая часть правила будет менятся так
1 user/$action/$id 2 user/profile/$id 3 user/profile/15
На первый взгляд - это бессмысленные действия. Какую строку пустили на вход /user/profile/15
такую и получили на выходе правила
Но это только на первый взгляд. На выходе мы получили стандартизованный URI, применив к которому операцию explode, получим в первом элементе название контроллера, во втором - метода, в тертьем - id
Для большей ясности, добавим в конец массива правил в config.php, правило
'([a-z0-9+_\-]+)\.html' => 'page/read/$id',
В результате работы этого правила, наш роутер любуй запрос html страницы (к примеру /about.html
без указания контроллера и метода), направит в контроллер page и метод read, а в параметр id пойдет строка about
Мы используем это для создания примера блога на основе нашего фреймворка. Однако сперва, улучшим шаблонизатор
Шаблонизатор
Чтобы начать делать шаблон для нашего фреймворка, нам потрбуется какая-нибудь сверстанная html страничка. К примеру возьмем бесплатную тему с сайта бутсрапа
Жмите ctrl+u
в своем браузере и просто копируйте весь исходный код в файл viws/main.php
- это и есть шаблон. Но его еще нужно доработать. Удалим все подключения стилей и скриптов через html разметку. Мы будем подключать необходимые скрипты через php. Это дает определенную уверенность, что в любом шаблоне всегда будет присутствовать набор файлов вашего фреймворка, независимо от того, использовал ли их верстальщик при верстке.
Для стилей и скриптов в корне проекта создадим отдельную папку. Обычно ее называют assets
, а в ней подпапки - css
- для стилей, js
- для скриптов, и images
- для изображений. При этом последнюю папку нужно использовать только для тех изображений, которые учавствуют в дизайне сайта, а не в его наполнении. Для наполнения лучше выделить отдельную директорию - media/images
в корне сайта.
Скачем свежий бутстрап и последнюю версию jquery. Кладем их в папку assets/js/libs/
. Остальные стили темы вам придется скачать самостоятельно.
Все стили и скрипты лежат по своим папкам, но как же их подключить. В нашем шаблонизаторе нет таких методов. Их необходимо написать. Добавлять их мы будем в файл ideal/classes/controller.php
. Добавим следующие методы
private function addAsset($link, $where = 'head', $asset = 'script', $type = 'url')
public function addScript($link, $where = 'head')
public function addStyleSheet($link, $where = 'head')
public function addScriptDeclaration($link, $where = 'head')
public function addStyleSheetDeclaration($link, $where = 'head')
Писать код нам придется для метода addAsset, все остальные методы - это вызов addAsset с различными параметрами, и сделаны скорее для удобства.
Добавим в класс следующее поле
private $assets = array();
В нем будут храниться данные о всех подключаемых стилях и скриптах. Далее код addAsset
private function addAsset($link, $where = 'head', $asset = 'script', $type = 'url'){ $hash = md5('addScript'.$link.$where.$asset.$type); $where = $where=='head' ? 'head' : 'body'; $asset = $asset=='script' ? 'script' : 'style'; if (!isset($this->assets[$hash])) { $this->assets[$hash] = array('where'=>$where,'asset'=>$asset,'type'=>$type,'data'=>$link); } }
$hash - вычисляется лишь для того, чтобы не подключать к странице одни и те же скрипты дважды. $where - это место, куда мы подключаем скрипты. Перед </head> или перед </body> т.е. в начало или в конец документа.
Далее методы по порядку
public function addScript($link, $where = 'head'){ $this->addAsset($link, $where); } public function addStyleSheet($link, $where = 'head'){ $this->addAsset($link, $where, 'style'); } public function addScriptDeclaration($data, $where = 'head'){ $this->addAsset($data, $where, 'script', 'inline'); } public function addStyleSheetDeclaration($data, $where = 'head'){ $this->addAsset($data, $where, 'style', 'inline'); }
Метода addAsset лишь заполняет массив $assets данными, но эти данные еще необходимо применить. Поэтому перепишем метод render
. Ранее, он у нас выводил main.php. Это не совсем правильно. Сделаем для это цели еще один метод renderPage
public function renderPage($content){ $html = $this->_renderPartial($this->tplPath.'main.php',array('content'=>$content), false); $output = array('head'=>'','body'=>''); foreach ($this->assets as $item) { if ($item['asset'] == 'script') { if ($item['type']=='inline') { $output[$item['where']].='<script type="text/javascript">'.$item['data'].'</script>'."\n"; } else { $output[$item['where']].='<script type="text/javascript" src="'.$item['data'].'"></script>'."\n"; } }else{ if ($item['type']=='inline') { $output[$item['where']].='<style>'.$item['data'].'</style>'."\n"; } else { $output[$item['where']].='<link rel="stylesheet" href="'.$item['data'].'" type="text/css" />'."\n"; } } } if ($output['head']) { $html = preg_replace('#(<\/head>)#iu', $output['head'].'$1', $html); } if ($output['body']) { $html = preg_replace('#(<\/body>)#iu', $output['body'].'$1', $html); } echo $html; }
Метод используя данные из assets, генерирует необходимые теги подключения скриптов (либо удаленный файл, либо inline) и добавляет их в нужное место документа
Метод, будет вызываться внутри метода start класса App, после запуска и отработки контроллера.
Метод start из ideal/classes/App.php
function start(){ $default_config = include IDEAL.'config.php'; $custom_config = include APP.'config.php'; $this->config = new Model(array_merge_recursive($default_config, $custom_config)); $this->uri = new Model(Router::gi()->parse($_SERVER['REQUEST_URI'])); $controller = app::gi($this->uri->controller.'Controller'); ob_start(); $controller->__call('action'.$this->uri->action, array($this->uri->id)); $content = ob_get_clean(); if ($this->config->scripts and is_array($this->config->scripts)) { foreach ($this->config->scripts as $script) { $controller->addScript($script); } } if ($this->config->styles and is_array($this->config->styles)) { foreach ($this->config->styles as $style) { $controller->addStyleSheet($style); } } $controller->renderPage($content); }
Тут мы использовали наш новоиспеченный роутер, приисвоив результат его работы полю uri. Теперь в любом месте нашего фреймворка, можно получить следующую информацию
App::gi()->uri->controller
- название контроллераApp::gi()->uri->action
- название метода контроллераApp::gi()->uri->id
- некий идентификатор
И любые другие переменные запроса, также будут достапны, через это поле. Это можно использовать к примеру для определения активного пункта меню.
Далее уже знакомая нам комбинация ob_start - вызов метода контроллера - ob_get_clean, и результат работы контроллера, передается в renderPage
Еще вы должно быть заметили подключение неких скриптов и стилей на строках с 10 по 19. Эти те внешние стили и скрипты, которые будут подключатся через config.php. И сделано это скорее просто для удобства использования фреймворка. К примеру мы можем вынести подключение jquery и bootstrap "за скобки" в config.php и они будут подключатся к любой нашей теме, к любой странице, сгенерированной нашим фреймворком. Удобно, не находите?!
Учитывая структуру папок, которую мы организовали выше, в config.php мы допишем следующие два массива
... 'scripts'=>array( '/assets/js/libs/jquery-2.1.3.min.js', '/assets/js/libs/bootstrap/js/bootstrap.min.js', ), 'styles'=>array( '/assets/js/libs/bootstrap/css/bootstrap.min.css', '/assets/js/libs/bootstrap/css/bootstrap-theme.min.css', ), ...
Так как фреймворк уже принял достойный облик, то можно его выложить на всеобщее обозрение. Теперь его работу можно лицезреть здесь
Контроллер page
Когда мы добавили еще одно правило в наш роутер, упоминался некий контроллер page и его метод read. Он довольно прост, создадим его в папке application/controllers/PageController.php
<?php class PageController extends Controller{ function actionRead($id='index'){ $this->render('read', array('id'=>$id)); } }
файл views/page/read.php
<? include 'pages/'.$id.'.php'; ?>
там же создаем еще одну папку views/page/pages
, а в ней нужные страницы. К примеру about.php
Всего доброго! Заходите на сайт, оставляйте комментарии. Новые уроки будут появляться только после вопросов с вашей стороны, заданных в комментариях.
Исходные коды урока посмотреть и скачать
Исходные коды конечного фреймворка посмотреть и скачать
Комментарии
З.Ы. Мне кажется этот фреймворк, после релиза, станет популярным
зы. Спасибо!)
private $path_elements = array('controller','action','id');
в классе роутера. т.е. набор обязательных параметров
preg_match('#'.$rule.'#sui', -- вот зачем нужна "решетка" и sui? В Гугле не могу найти ничего подобного)
А в прошлом варианте parse() роутера было:
preg_match('#'.$regxp.'#Uuis', -- тоже непонятен смысл "решетки" и символов Uuis
Можно, я использовал более понятную и приятную форму метода parse:
$request = $_SERVER['REQUEST_URI'];
$splits = explode('/', trim($request, '/'));
$this->controller = !empty($splits[0]) ? ucfirst($splits[0])
. 'Controller' : App::gI()->config->default_controller.'Controller' ;
$this->action = !empty($splits[1]) ? $splits[1]
. 'Action' : App::gI()->config->default_action.'Action';
if (!empty($splits[2])) {
$keys = $values = array();
for ($i = 2, $cnt = count($splits); $i < $cnt; $i++) {
if ($i % 2 == 0) {
$keys[] = $splits[$i];
} else {
$values[] = $splits[$i];
}
}
$this->params = array_combine($keys, $values);
}
$this->render('theme/red/read', array('id'=>$id));
сделал так, в классе App
if (($this->uri->{'controller'} == 'controller') && ($this->uri->{'action'} == 'view')) {
$controller->renderPageController($content, $this->uri->{'id'});
} else {
$controller->renderPage($content);
}
и в классе Controller добавил измененный renderPage()