PHP

Быстрый роутинг на PHP


Уходя от использования роутинга в .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>
Теги:
роутинг, mvc
Добавлено: 15 Сентября 2013 01:15:18 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...