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

Всего доброго! Заходите на сайт, оставляйте комментарии. Новые уроки будут появляться только после вопросов с вашей стороны, заданных в комментариях.

Исходные коды урока посмотреть и скачать

Исходные коды конечного фреймворка посмотреть и скачать

Оставлять комментарии могут только зарегистрированные пользователи

Комментарии  

ВасилийВасильевич
# ВасилийВасильевич 17.02.2015 12:39
Спасибо за статью, прояснились некоторые моменты. Хотелось бы увидеть пример обработки ошибок. Класс Except есть, но он пустой.
З.Ы. Мне кажется этот фреймворк, после релиза, станет популярным :-)
Leroy
# Leroy 17.02.2015 13:54
да, планировал в следующей статье про ошибки поговорить.
зы. Спасибо!)
Leroy
# Leroy 18.02.2015 15:52
Статья про обработку ошибок xdan.ru/kak-napisat-svoj-frejmvork-na-php-obrabotka-oshibok.html
Игорь
# Игорь 22.04.2015 20:12
Что за свойство $this->path_elements[$i] в методе роутинга (function parse($path)) ?
Leroy
# Leroy 23.04.2015 09:39
сори, забыл про него
private $path_elements = array('controller','action','id');
в классе роутера. т.е. набор обязательных параметров
Никита
# Никита 01.05.2015 12:02
Добрый день! Объясните пожалуйста вот такую штуку в parse() роутера:

preg_match('#'.$rule.'#sui', -- вот зачем нужна "решетка" и sui? В Гугле не могу найти ничего подобного)

А в прошлом варианте parse() роутера было:
preg_match('#'.$regxp.'#Uuis', -- тоже непонятен смысл "решетки" и символов Uuis
Никита
# Никита 01.05.2015 12:37
Нельзя ли вообще обходится здесь без этой мерзости - регулярных выражений?
ArtemV
# ArtemV 03.07.2015 02:32
Цитирую Никита:
Нельзя ли вообще обходится здесь без этой мерзости - регулярных выражений?

Можно, я использовал более понятную и приятную форму метода 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);
}
Casper
# Casper 18.11.2015 07:49
Здравствуйте. Скажи, пожалуйста, а можно ли сделать разный дизайн (шаблон), для разных страниц
Casper
# Casper 20.11.2015 11:35
Здравствуйте. Скажи, пожалуйста, а можно ли сделать разный дизайн (шаблон), для разных страниц
Leroy
# Leroy 20.11.2015 12:47
Ну в чем проблема? Открытый исходный код. Делаем разные папки, с разными темами и вперед.
$this->render('theme/red/read', array('id'=>$id));
Casper
# Casper 20.11.2015 13:22
Это то да, но мне нужно общий дизайн изменить

сделал так, в классе App
if (($this->uri->{'controller'} == 'controller') && ($this->uri->{'action'} == 'view')) {
$controller->renderPageController($content, $this->uri->{'id'});
} else {
$controller->renderPage($content);
}

и в классе Controller добавил измененный renderPage()
Vyaches
# Vyaches 20.01.2020 06:56
Отличная статья!