
Уходя от использования роутинга в .htaccess файле, в первую очередь пришёл к стандартному направлению на index.php: разбирал там URL и вызывал соответствующие контроллеры — долгое время был доволен такой техникой. Однако совсем недавно осознал, что что-то делаю не так, что можно сделать эффективнее и лучше.
Далее я расскажу о своём роутинге, использующем XML для хранения правил и в последующем использующем его сериализованный вид.
Уходя от проверки в индексном файле, хотел в первую очередь избавиться от подобной конструкции
if ($url[1] == 'news'){}
Мне не очень хотелось использовать конструкции, предлагаемые большинством фреймворков, в целом выглядящие в таком виде:
$route->addRule(
'/news/{id}/',
array(
'controler' = 'news',
'action' = 'showOne'
)
);
Изначально хотелось хранить правила роутинга в JSON или XML.
Однако парсить каждый раз файл не очень хорошая идея, и такой тип более пригоден для статической навигации или навигации вида /controller/action/.
Мне же хотелось большей гибкости в настройке роутинга и в конечном итоге решил использовать XML для хранения правил, а после парсинга файла и создания массива правил сериализовывать его в файл (в дальнейшем используя его для получения настроек)
XML-файл правил роутинга выглядит примерно так:
<root>
<routes>
<route match="exit" controller="user" action="exit" />
<route match="secret" controller="error" action="404">
<route match="love" controller="secret" action="love" />
</route>
<route match="user" controller="user" action="list">
<route controller="user" action="user" match="{login}">
<route match="comments" controller="user" action="comments" />
<route match="wall" controller="user" action="wall" />
</route>
</route>
</routes>
</root>
Структура правила представляет собой следующее
XML-элемент правил содержится в элементе /root/routes, элемент правил должен содержать в себе следующие атрибуты:
match — Используется для поиска по URL
controller — Вызываемый контроллер
action — Вызываемый метод
match может содержать как статические данные, например «secret», так и динамические «page-{page|num}», динамические отличаются от статических наличием фигурных скобок и названием переменной в ней (название переменной и её значение будут получены в случае совпадения)
В переменной можно указать её тип:
{param1|num} — выдаст совпадение только в случае, если param1 является числовым значением
{param2|str} — выдаст совпадение только в случае, если param2 содержит в себе только буквы и цифры
На основе XML формируется массив, который разделает статические и динамические правила.
Все потомки так же разделяются на статических и динамических.
Так же в элементе /root/system
хранятся следующие данные:
<route match="index" controller="index" action="index" />
<route match="not_found" controller="error" action="404" />
Соответственно в случае, если совпадения по правилам роутинга найдены не будут, вернётся 404 ошибка, в случае пустого урл — его index значение
Использование выглядит так:
$result = $router->get('/secret/love/');
Результат будет таким:
Array
(
[controller] => secret
[action] => love
[values] => Array
(
)
)
$result = $router->get('/user/Testik/wall/');
Array
(
[controller] => user
[action] => wall
[values] => Array
(
[login] => Testik
)
)
Файлы
index.php
<?php
include 'routing.class.php';
$router = new routing('routing.xml');
echo '<pre>';
$result = $router->get('/');
print_r($result);
$result = $router->get('/secret/love');
print_r($result);
$result = $router->get('/megapage/not/isset');
print_r($result);
$result = $router->get('/user/testik/');
print_r($result);
echo '</pre>';
routing.class.php
<?php
class routing
{
private $route = array();
/*
* @param string $file - Name of XML file with rules
* @param bool $debug - If true, don't use the serialized data
*/
public function __construct ($file, $debug = false)
{
if (!$debug && file_exists($file . '.dat') ) {
$this->route = unserialize(file_get_contents($file . '.dat'));
} elseif (file_exists($file) ) {
$result = $this->get_routing(simplexml_load_file($file));
$this->route = $result;
$serialized = serialize($result);
file_put_contents($file . '.dat', $serialized);
} else {
throw new Exception("file not found");
}
}
/*
* @param string $url
* @return array
*/
public function get($url)
{
$url = array_filter(explode('/', $url), 'mb_strlen');
if (empty($url)) {
$result = $this->index();
} else {
$result = $this->get_params($url, $this->route['route']);
if (empty($result)) {
$result = $this->not_found();
}
}
return $result;
}
/*
* @return array
*/
public function not_found()
{
$route = $this->route['system']['not_found'];
$query_values = array();
if (isset($route['values'])) {
$query_values = $route['values'];
}
return array (
'controller' => $route['params']['controller'],
'action' => $route['params']['action'],
'values' => $query_values,
);
}
/*
* @return array
*/
public function forbidden()
{
$route = $this->route['system']['forbidden'];
$query_values = array();
if (isset($route['values'])) {
$query_values = $route['values'];
}
return array (
'controller' => $route['params']['controller'],
'action' => $route['params']['action'],
'values' => $query_values,
);
}
/*
* @return array
*/
public function index()
{
$route = $this->route['system']['index'];
$query_values = array();
if (isset($route['values'])) {
$query_values = $route['values'];
}
return array (
'controller' => $route['params']['controller'],
'action' => $route['params']['action'],
'values' => $query_values,
);
}
/*
* @param array $match Array of url data
* @param array $route An array of routing data
* @return array|false
*/
private function get_params(& $match, $route)
{
if (!empty($match)){
$result = false;
$string = array_shift($match);
$array_link = false;
$query_values = array();
if (isset ($route[0][$string]) ) {
$array_link = $route[0][$string];
} else {
foreach ($route[1] as $key => &$value) {
if (isset($value['params']['regex'])) {
if (preg_match($value['params']['regex'], $string, $temp_values)) {
$array_link = &$value;
foreach ($temp_values as $k =>$finded) {
if (isset ($value['params']['values'][$k]) ) {
$query_values[$value['params']['values'][$k]] = $finded;
}
}
break;
}
}
}
}
if ($array_link) {
if (empty($match)){
return array (
'controller' => $array_link['params']['controller'],
'action' => $array_link['params']['action'],
'values' => $query_values,
);
} elseif (!empty ($array_link['items']) ) {
$sub_result = $this->get_params($match, $array_link['items']);
if ($sub_result && !empty($query_values)) {
$sub_result['values'] = array_merge($query_values, $sub_result['values']);
}
return $sub_result;
}
}
return false;
}
}
/*
* @param SimpleXMLElement $xml Contents of the xml file with routing rules
* @return array|false
*/
private function get_routing($xml)
{
$routes = $xml->xpath('/root/routes');
$route['route'] = $this->build_routing($routes[0]);
$temp = $xml->xpath('/root/system');
$temp = $this->build_routing($temp[0]);
$route['system'] = $temp[0];
return $route;
}
/*
* @param SimpleXMLElement $xml The xml node
* @return array|false
*/
private function build_routing ($xml)
{
$routes = false;
foreach ($xml->children() as $route) {
$params = array();
$result_route = array();
$element_name = (string)$route['match'];
$type = 0;
foreach($route->attributes() as $key => $value) {
$params[(string)$key] = (string)$value;
}
if ($route->count() > 0) {
$sub_items = $this->build_routing($route);
if ($sub_items) {
$result_route['items'] = $sub_items;
}
}
if(strpos($params['match'], '{') !== false) {
preg_match_all('/\{(.+)(\|num|\|str|)\}/siuU', $params['match'], $match_attrs, PREG_SET_ORDER);
foreach($match_attrs as $key => $varible) {
$params['match'] = str_replace("{" . $varible[1] . "}", '(.+)', $params['match']);
$params['match'] = str_replace("{" . $varible[1] . "|num}", '(\d*)', $params['match']);
$params['match'] = str_replace("{" . $varible[1] . "|str}", '([а-яёa-z1-9]*)', $params['match']);
$params['values'][($key+1)] = $varible[1];
}
$params['match'] = '/^' . $params['match'] .'$/iu';
$params['regex'] = $params['match'];
$element_name = crc32($params['match']);
$type = 1;
}
$result_route['params'] = $params;
$routes[$type][$element_name] = $result_route;
}
return $routes;
}
}
routing.xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<system>
<route match="index" controller="index" action="index" />
<route match="forbidden" controller="error" action="403" />
<route match="not_found" controller="error" action="404" />
</system>
<routes>
<route match="auth" controller="user" action="auth" />
<route match="register" controller="user" action="register" />
<route match="exit" controller="user" action="exit" />
<route match="secret" controller="error" action="404">
<route match="love" controller="secret" action="love" />
</route>
<route controller="partition" action="list" match="part-{partition_url}-{date|num}">
<route ontroller="partition" action="list" match="page-{page}" />
<route controller="post" action="post" match="{post_url}" />
</route>
<route controller="partition" action="list" match="part-{partition_url|str}">
<route controller="partition" action="list" match="page-{page|num}" />
<route controller="post" action="post" match="{post_url}" />
</route>
<route match="page" controller="page" action="list">
<route controller="page" action="list" match="page-{page}" />
<route controller="page" action="page" match="{id}" />
</route>
<route match="user" controller="user" action="list">
<route controller="user" action="user" match="id{user_id|num}">
<route match="comments" controller="user" action="comments" />
<route match="wall" controller="user" action="wall" />
</route>
<route controller="user" action="user" match="{login}">
<route match="comments" controller="user" action="comments" />
<route match="wall" controller="user" action="wall" />
</route>
</route>
</routes>
</root>