Stephan Romhart über Technik, Kultur und Philosophie

Philosophie · Technik

Performantes URL-Routing – mit Php und regulären Ausdrücken (RegEx)

Stephan 0

Eines vorweg: Du solltest Gundkenntnisse zu Php, Htaccess und RegEx besitzen. Falls Dir das alles nichts sagt, ist mein Tutorial mit großer Wahrscheinlichkeit nutzlos. Ganz unten findst Du weiterführende Links zum Thema "Reguläre Ausdrücke" und "htaccess/Mod_Rewrite".

Zu Anfang kurz erklärt: Was macht ein Url-Router? Ein Url-Router wertet die angefragte Url aus und interpretiert diese nach unseren Vorgaben. Ich benutze den Router in meinen Projekten, um per Url einen bestimmten Controller zu laden. Ein Beispiel:

http://www.website.de/user/edit/4

Diese Url soll den Controller "user" mit der Action "edit" und dem Parameter "4" aufrufen.

include "ctrl/user-edit.ctrl.php";

Ein weiteres Beispiel:

http://www.domain.de/company/search/autocomplete/german

Diese Url soll den Controller "company" mit der Action "search-autocomplete" und dem Filter "german" aufrufen.

include "ctrl/company-search-autocomplete.ctrl.php";

In beiden Beispielen habe ich noch Parameter wie die User-Id oder den Filter. Diese sollten an das Php-Skript übergeben werden können und sind nicht Teil des Controller-Names, der sich aus der Url ergeben soll.

Schritt 1: Die htaccess-Datei

Damit der Teil nach "http://www.domain.de/" von Php gelesen werden kann, benötigt man eine htaccess-Datei, die eine Umschreibungsanweisung an den Webserver schickt. Im Beispiel erfolgt das per ModRewrite-Modul, was auf den meisten Apache/Php/MySQL-Webservern Standard ist.

# ModRewrite aktivieren
RewriteEngine On

# ModRewrite nicht bei Dateien oder Ordnern greifen lassen
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# Url umschreiben
RewriteRule (.+) index.php?path=$0 [L,QSA]

In der letzten Zeile steht, wie der Teil nach "http://www.domain.de/" an das Php-Skript geliefert wird: als Get-Parameter "path".

Schritt 2: Die Routes

Damit das Routing funktioniert, benötigt das Skript die Infos, welche Routen existieren. Im Beispiel definiere ich drei Routen und packe alles in eine Datei "index.php". Bei größeren Projekten kann man die Routes auch in ein Config-File stecken.

<?php

$routes = array(
	array(
		'path_pattern' => '/^index$/',
		'controller'   => 'ctrl/index.ctrl.php'
	),
	array(
		'path_pattern' => '/^user\/edit\/(?P<user_id>\d+)$/',
		'controller'   => 'ctrl/user-edit.ctrl.php'
	),
	array(
		'path_pattern' => '/^company\/search\/autocomplete\/(?P<filter>[a-z_]+)$/',
		'controller'   => 'ctrl/company-search-autocomplete.ctrl.php'
	)
);

?>

Die Routen sind ein dreifach verschachteltes Array. Der Wert "path_pattern" ist ein Regulärer Ausdruck, der eine Musterformulierung darstellt, die allgemein für eine Url gilt. Der Wert "controller" ist in diesem Beispiel ein Pfad zu einer Controller-Datei.

Schritt 3: Die Router-Funktion

Ich zeige nun die komplette Funktion des Routers am Stück und gehe sie dann Schritt für Schritt durch, um zu erklären, was die einzelnen Code-Blöcke machen.

function router($routes)
{
	$route_match = false;
	$url_path    = 'index';
	$url_params  = array();
	
	if(isset($_GET['path']))
	{
		$url_path = $_GET['path'];
		if(substr($url_path,-1) == '/')
		{
			$url_path = substr($url_path,0,-1);
		}
	}
	
	foreach($routes as $route)
	{
		if(preg_match($route['path_pattern'],$url_path,$matches))
		{
			$url_params  = array_merge($url_params,$matches);
			$route_match = true;
			break;
		}
	}
	
	if(!$route_match)
	{
		exit('Der Url-Pfad "'.$url_path.'" ist nicht definiert.');
	}
	
	if(file_exists($route['controller'])
	{
		include($route['controller']);
	}
	else
	{
		exit('Der Controller "'.$route['controller'].'" existiert nicht.');
	}
}

Die Funktion

function router($routes)
{
}

Ich habe die Funktion hier "router" genannt. Parameter ist das zuvor erstellte Array "routes".

Die Basis-Variablen

function router($routes)
{
	$route_match = false;
	$url_path    = 'index';
	$url_params  = array();
}

"route_match" ist eine booleansche Variable, die erstmal auf "false" gesetzt wird. Bis zum Zeitpunkt der Ausführung der Funktion passt also noch keine Route. Die Variable "url_path" setze ich inital auf den Wert "index". Zur Speicherung der möglichen Url-Parameter initalisiere ich das Array "url_params".

Der Get-Parameter path

function router($routes)
{
	$route_match = false;
	$url_path    = 'index';
	$url_params  = array();
	
	if(isset($_GET['path']))
	{
		$url_path = $_GET['path'];
		if(substr($url_path,-1) == '/')
		{
			$url_path = substr($url_path,0,-1);
		}
	}
}

Hier prüfe ich, ob überhaupt eine Get-Variable existiert. Falls ja, überschreibe ich "url_path" mit dem Wert aus dem Get-Parameter "path". Die if-Bedingung "if(substr($url_path,-1) == '/')" schneidet das letzte Zeíchen des Strings "url_path" ab, falls es ein "/" ist.

Die Routen mit RegEx überprüfen

function router($routes)
{
	$route_match = false;
	$url_path    = 'index';
	$url_params  = array();
	
	if(isset($_GET['path']))
	{
		$url_path = $_GET['path'];
		if(substr($url_path,-1) == '/')
		{
			$url_path = substr($url_path,0,-1);
		}
	}
	
	foreach($routes as $route)
	{
		if(preg_match($route['path_pattern'],$url_path,$matches))
		{
			$url_params  = array_merge($url_params,$matches);
			$route_match = true;
			break;
		}
	}
}

Mit der foreach-Schleife laufe ich die Variable "routes" durch. Die If-Bedingung "if(preg_match(...))" prüft, ob das Muster des aktuellen Schleifenelements zur Variable "url_path" passt und übergibt gleich noch die Treffer-Variablen mit der Variable "matches". Die Variable "url_params" beinhaltet dann Parameter, die in der Url enthalten waren.

Es erfolgt noch eine setzten der Variable "route_match" auf "true" und ein "break" für die foreach-Schleife.

Die Auswertung der Variablen

function router($routes)
{
	$route_match = false;
	$url_path    = 'index';
	$url_params  = array();
	
	if(isset($_GET['path']))
	{
		$url_path = $_GET['path'];
		if(substr($url_path,-1) == '/')
		{
			$url_path = substr($url_path,0,-1);
		}
	}
	
	foreach($routes as $route)
	{
		if(preg_match($route['path_pattern'],$url_path,$matches))
		{
			$url_params  = array_merge($url_params,$matches);
			$route_match = true;
			break;
		}
	}
	
	if(!$route_match)
	{
		exit('Der Url-Pfad "'.$url_path.'" ist nicht definiert.');
	}
	
	if(file_exists($route['controller']))
	{
		include($route['controller']);
	}
	else
	{
		exit('Der Controller "'.$route['controller'].'" existiert nicht.');
	}
}

Anschließen prüfe ich, ob "route_match" immer noch "false" ist und mache hier beispielhaft einen Exit mit der Fehlermeldung, dass die Route nicht gefunden wurde. Falls eine Route gefunden wurde, prüfe ich noch, ob die Controller-Datei aus der Routerangabe existiert und include sie.

Das komplette Script

Mit einem simplen "router($routes);" kann der Router nun aufgerufen werden. Bei der Url "http://www.website.de/user/edit/4" wird nun der Controller "ctrl/user-edit.ctrl.php" geladen und im assoziativen Array "url_params" gibt es nun den Eintrag "user_id" => 4, auf den das Include zugreifen kann.

Der Key "user_id" kommt aus der Pattern "/^user\/edit\/(?P\d+)$/".

<?php

$routes = array(
	array(
		'path_pattern' => '/^index$/',
		'controller'   => 'ctrl/index.ctrl.php'
	),
	array(
		'path_pattern' => '/^user\/edit\/(?P<user_id>\d+)$/',
		'controller'   => 'ctrl/user-edit.ctrl.php'
	),
	array(
		'path_pattern' => '/^company\/search\/autocomplete\/(?P<filter>[a-z_]+)$/',
		'controller'   => 'ctrl/company-search-autocomplete.ctrl.php'
	)
);

function router($routes)
{
	$route_match = false;
	$url_path    = 'index';
	$url_params  = array();
	
	if(isset($_GET['path']))
	{
		$url_path = $_GET['path'];
		if(substr($url_path,-1) == '/')
		{
			$url_path = substr($url_path,0,-1);
		}
	}
	
	foreach($routes as $route)
	{
		if(preg_match($route['path_pattern'],$url_path,$matches))
		{
			$url_params  = array_merge($url_params,$matches);
			$route_match = true;
			break;
		}
	}
	
	if(!$route_match)
	{
		exit('Der Url-Pfad "'.$url_path.'" ist nicht definiert.');
	}
	
	if(file_exists($route['controller']))
	{
		include($route['controller']);
	}
	else
	{
		exit('Der Controller "'.$route['controller'].'" existiert nicht.');
	}
}

router($routes);

?>

In meinem Beispiel habe ich die grundlegende Idee gezeigt. Mit dem Ansatz kann man einen sehr flexiblen Router erstellen. Das Array der Routes ist um diverse Infos erweiterbar und das komplette Handling der gewonnen Infos aus der Url ist frei gestaltbar.

Ich habe diverse Projekte mit dieser Systematik umgesetzt und bin mit der Performance sehr zufrieden. Was denkt Ihr über das Skript und die Vorgehensweise? Ich freue mich über Feedback!

Weiterführende Links