Tutorial: Ein FrontController in PHP

Hallo,

da ich jetzt schon des öfteren hier Threads gelesen wie z.B. diesen oder den hier, hab ich mir gedacht ich schreib mal ein Tutorial wie man sowas lösen könnte.

In diesem Tutorial zeig ich euch wie man einen Front Controller, einen AutoLoader und eine ganz kleine Template Klasse in PHP (version 5) schreibt.

Zuerst legen wir die Verzeichnisstruktur und die benötigten Dateien an.

/
| - index.php
| - .htaccess
| + core
| |- FrontController.class.php
| |- AutoLoader.class.php
| |- Template.class.php
| |- Action.class.php
|+ modules
|+ main
|+ actions
| |- Main.class.php
|+ classes
|+templates
| |- Main.tpl.php

Was wofür ist wird später erklärt. Aber schon mal ein kurzes Vorwort zum core und modules Verzeichnis, im core werden nur wichtige Klassen und Libriers abgelegt. Im modules Verzeichnis werden die eigentlichen "Seiten" der Homepage abgelegt. Dazu aber später mehr.

So, jetzt haben wir alle Verzeichnisse und Dateien angelegt die wir brauchen. Beginnen wir also mit der implementierung des Front Controllers.

"Liebe Bastelfreunde, ich hab da schon mal was vorbereitet." ;)

FrontController.class.php
PHP:
<?php
//Hier includieren wir den AutoLoader, den wir aber erst später implementieren
require_once('core/AutoLoader.class.php');

class FrontController {
	
	//Hier wird der Pfad zum Wurzelverzeichnis gespeichert
	private $rootPath;
	
	//Singleton Pattern -> Hier wird eine Instanz der FrontControllers gespeichert
	private static $instance = null;
	
	//Hier wird die Action festgelegt die nach dem Aufruf der Seite angezeigt werden soll. 
	//Das ganze könnte man natürlich auch aus einer Config lesen.
	private $firstAction = 'main.Main';
	
	//In dieser Variable wird immer die letzte Action gespeichert
	private $lastAction;
	
	//Der Konstruktor erwartet als Parameter den Pfad zum Wurzelverzeichnis
	public function __construct($rootPath) {
		$this->rootPath = $rootPath;
		//Alle Klasse die man ständig braucht, werden hier geladen.
		AutoLoader::addPackage('core/Template.class.php');
		AutoLoader::addPackage('core/Action.class.php');
	}
	
	public function __destruct() {
	
	}
	
	// Der Front Controller wird als Singleton Pattern implementiert, da wir ja nur eine Instanz pro Request haben wollen.
	public static function getInstance($rootPath = null) {
		// Falls es noch keine Instanz des FrontControllers gibt, wird hier eine instanziert.
		if (! is_object(self::$instance)) {
			if (is_null($rootPath)) {
					throw new Exception('Es muss der Rootpfad angegeben werden!');
			}
			// Instanzieren des Front Controllers 
			self::$instance = new FrontController($rootPath);
		}
		// Instanz des FrontControllers zurück geben.
		return self::$instance;
	}
	
	// Die run Methode wird bei jedem Request aufgerufen.
	public function run() {
		// Hier die aufgerufene Action aus dem Request gelesen.
		$completeName = $_REQUEST['action'];
		
		// In diesem Codeblock wird geprüft, ob eine Action übergeben/aufgerufen wurde oder nicht.
		// Wenn keine Action übergeben/aufgerufen wurde, wird die First Action ausgeführt.
		try {
			if (! empty($completeName)) {
				$this->executeAction($completeName);
			} else {
				$this->executeAction($this->firstAction);
			}
		} catch (Exception $e) {
			echo $e->getMessage();
		}
	}
	
	//Diese Methode ist das eigentlich Kernstück des Front Controllers.
	// Hier wird geschaut ob es das aufgerufene Module und die Action 
	// überhaupt gibt.
	private function executeAction($completeName) {
		// Hier wird der Request String in seine einzelteile zerlegt.
		// Ein Aufruf einer Action sieht folgendermaßen aus,
		// main.Main.html
		//  |         |        |- Wird durch mod-rewrite schon abgeschnitten
		//  |         |- Die Action die ausgeführt werden soll
		//  |- Das Module in der sich die Action befindet
		$actionName = self::getActionName($completeName);
		$moduleName = self::getModuleName($completeName);
		
		// Hier wird geprüft ob es das Module überhaupt gibt, falls nicht wird eine Exception geworfen
		if (! is_dir('./modules/' . $moduleName)) {
			throw new Exception('Das angegebene Modul existiert nicht!');
		}
		
		// Prüfen, ob es die Action überhaupt gibt, falls nicht wirf dem User eine Exception vor die Füße
		if (! is_file('./modules/' . $moduleName . '/actions/' . $actionName . '.class.php')) {
			throw new Exception('Die angegebene Action existiert nicht!');
		}
		
		// Action includieren
		require_once('./modules/' . $moduleName . '/actions/' . $actionName . '.class.php');
		
		// Neue Instanz der Action erzeugen
		$action = new $actionName();
		if (! is_object($action)) {
			throw new Exception('Die Action konnte nicht initialisiert werden!');
		}
		
		// Action ausführen
		try {
			$action->run();
		} catch (Exception $e) {
			echo $e->getMessage();
		}
		//lastAction mit der aktuell aufgerufenen Action ersetzen
		$this->lastAction = $completeName;
	}
	
	// Diese Methode brauchen wir später fürs Template
	public function includeAction($completeName) {
		$this->executeAction($completeName);
	}
	
	//Extrahiert den Actionname aus dem Request String
	public static function getActionName($completeName) {
		return substr($completeName, strrpos($completeName, ".") + 1);
	}
	
	//Extrahiert den Modulename aus dem Request String
	public static function getModuleName($completeName) {
		return substr($completeName, 0, strrpos($completeName, "."));
	}
}
?>

Ok, das war schonmal der Front Controller. Jetzt erstellen wir noch schnell die index.php

PHP:
<?php

//Auslesen des Wurzelverzeichnisses
$root = dirname(__FILE__);

//FrontController includieren
require_once($root . '/core/FrontController.class.php');

// Instanzieren des Front Controllers
$main = FrontController::getInstance($root);
if (is_object($main)) {
	// Front Controller starten
	$main->run();
}

?>

Und das ist die index.php, mal überlegen ob ich alles für den ersten Teil habe. Ach ja, stimmt uns fehlt ja noch die .htaccess Datei.

.htaccess
Code:
php_flag register_globals off

# Umschreiben der *.html-URLs in entsprechende action-Parameter:
RewriteEngine on
RewriteRule ^([^/]+)\.html$ index.php?action=$1&mod_rewrite=true [QSA]

Das wars dann auch schon mit dem ersten Teil des Tutorials.
Ich hoffe mal der eine oder andere kann was damit anfangen.

Fragen könnt Ihr ja hier posten oder mir per PM schicken.
 
Schöne Sache, toll gemacht, obwohl ich eigentlich Ruby bevorzugen würde (Nicht dass ich was gegen PHP hätte, aber Ruby ist einfach bequemer^^). Hoffe auf weitere Tutorials^^.

mfg

JTron
 
das klingt sehr interessant.
kannst du mal die .ht files erläutern? ich bin da leider noch nicht sonderlich fit drin! ;)
also was der code macht ist mir klar, aber bitte mal die syntax erläutern...
 
Guten Morgen,

hier mal die Erklärung:

"php_flag register_globals off"
Gibt nur an, dass register_globals ausgeschaltet werden soll. Ich geh jetzt nicht näher drauf ein, da ich glaube Du weisst was das bedeutet.

# Umschreiben der *.html-URLs in entsprechende action-Parameter:
RewriteEngine on
Dieses Zeile schaltet die RewriteEngine ein.

RewriteRule ^([^/]+)\.html$ index.php?action=$1&mod_rewrite=true [QSA]

Ok, und hier ist die eigentliche Regel:

^([^/]+)\.html$ --> Ist ein ganz normaler Regulärer Ausdruck.
^ Anfang des Regex
() Definiert eine Gruppe, auf die man mit $n zugreifen kann
[^/] Kein Slash
+ steht für ein beliebiges Zeichen
\.html Am Ende soll ein Punkt und html kommen
$ Ende des Regex

Wie das dann genau ausschaut, zeig ich Dir an einem Beispiel:

main.Main.html wird aufgerufen

Unser Regex trifft zu und formt das ganze zu folgender URL um:

index.php?action=main.Main&mod_rewrite=true

$1 wurde mit der Gruppe in unserem Regex ersetzt. Soweit so gut, nur was passiert wenn wir Parameter übergeben?
Genau, Sie werden ignoriert.

Deshalb steht am Ende folgendes [QSA] = Query String Append. Jetzt werden auch die Parameter mit angehängt.

Der Parameter mod_rewrite=true wird in diesem Tutorial nicht benutzt, man kann Ihn auch weglassen.

So, ich hoffe es ist verständlich erklärt.
 
ah vielen dank!
aber wenn ich schon das ganze mit htmls mache, wäre es dochschon sinvoller, sämmtliche angaben vor dem .html mit zu verbauen, das wäre für suchmachinen doch sinvoller.

zb sowas dann wie seite-1-hier_kommt_der_themen_name.html

btw, wie ist das, wenn ich das [^/] rausnehmen und damit die slashes zulasse, würde da automatisch die www.domain.com/seite/2/name_des_themas.html auf die /index.php umgeschrieben oder muss ich da ne spezielle configuration mit vornehmen?
 
Jein, hier sagen einige dass solche URLs von Google besser geranked werden, andere wiederrum sagen es sei Google egal.

Und hier geht es ja nur darum, die Module+Actions auf vernüftige URLs zu bringen.
Rufst Du z.B. folgende Seite auf, main.Main.html, wird dem FrontController durch die ModRewrite Rule folgendes übergeben, main.Main, der FrontController kann jetzt nachschauen, ob es ein Modul mit dem Namen "main" und der Action "Main" gibt, und diese starten.

btw, wie ist das, wenn ich das [^/] rausnehmen und damit die slashes zulasse, würde da automatisch die www.domain.com/seite/2/name_des_themas.html auf die /index.php umgeschrieben oder muss ich da ne spezielle configuration mit vornehmen?

Ja, das ganze würde dann so aussehen: www.domain.com/seite/2/name_des_themas.html --> www.domain.com/index.php?action=/seite/2/name_des_themas

Womit aber der Frontcontroller in dieser Version nichts anfangen könnte.
 
nuja so würde ich dass dann evrwenden, das kannsch mir ja anpassen, das sollte kein problem sein :)
meine siginfo.de seite soll da nicht so modular werden, da dass ja nur nen privatprojekt ist.
 
ich bin mir noch nicht sicher ob ich deine version hier nutze oder mir das selbst schreibe und ab und zu mir mal ne anregung von dir hole.

was sich dabei im übrigen ergbit, warum die exceptions? was genau haben die für vorteile? hab mit denen noch nie gearbeitet
 
Exceptions haben den Vorteil, dass man mit Ihnen eine Saubere Fehlerbehandlung machen kann.

Die meisten benutzen die() oder arbeiten mit errorlevels die Sie mit return oder so zurück geben. Was bei OOP aber nicht gerade sehr schön ist. Exceptions sollten vorallem dort geworfen werden, wo man schon von vornherein weiss hier könnte es krachen.

Mit Exceptions kannst Du z.B. folgendes machen:
PHP:
try {
    $test = new Test();
    $test->system('test');
    $test->openFile('gibts nicht'); // wirft eine FileException -> throw new FileException('Bad file!');    
} catch (SystemException $se) {
    echo $se->getMessage();
} catch (FileException $fe) {
   echo $fe->getMessage();
   unset($test); // Hier wird das Objekt zerstört
} catch (Exception $e) {

}

So kannst Du z.B. eine Reihe von Anweisungen in einen try-catch-block setzen, und falls jetzt irgend wo eine Exception geworfen wird, diese fangen und z.B. den Programmfluss weiterlaufen lassen usw.. Man kann sich halt mit Exceptions ein sauberes ErrorHandling aufbauen.
 
irgendwie... ich bin verwirrt.

PHP:
<?php
 class test {
   function openfile($arg) {
     fopen($arg,"r");
   }
 }

try {
    $test = new Test();
    $test->openFile('gibts nicht');
}
catch (SystemException $se) {
    #echo $se->getMessage();
    echo "hallo";
}
?>

müsste da nich eingentlich nur "hallo" bei rumkommen?

es kommt aber:
Code:
PHP Warning:  fopen(gibts nicht): failed to open stream: No such file or directory in E:\asd.php on line 4
 
ah, bin selbst drauf gekommen ... :D my fault!


was ist eigentlich der vorteil der instanzierung anstatt der initialisierung via $controller = new Frontcontroller(); ?
 
Unter Windows wird der Destruktor nicht gerufen. Abhilfe schafft das Setzen einer Shutdown-Funktion


PHP:
.
.
.
public function __construct($rootPath) {
   $this->rootPath = $rootPath;
   //Alle Klasse die man ständig braucht, werden hier geladen.
   AutoLoader::addPackage('core/Template.class.php');
   AutoLoader::addPackage('core/Action.class.php');
   
   //Fix für Windows Systeme
   register_shutdown_function(array(&$this, '__destruct'));
}
    
public function __destruct() {
   
}
.
.
.
 
Original von spy
Unter Windows wird der Destruktor nicht gerufen. Abhilfe schafft das Setzen einer Shutdown-Funktion

sicher, dass es an Windows liegt und nicht an der PHP-Version?
Welche Version verwendest du? nutzt du XAMPP? oder Apache, Mysql und php manuell installiert?
Hab das ehrlich gesagt noch nie vorher gehört.... werd sicherlich nachher mal ne VirtualBox mit Windows + XAMPP aufsetzen und das ganze selber probieren....
 
Jetzt hast du mich unsicher gemacht. Hab gerade mal google was arbeiten lassen und es könnte tatsächlich an der PHP Version liegen. Lass dich aber nicht davon abhalten das mal zu testen :)

ja xampp und zwar heute morgen noch die aktuellste Version installiert
 
Unter Windows wird der Destruktor nicht gerufen. Abhilfe schafft das Setzen einer Shutdown-Funktion

Das wäre mir ganz neu. Also bei meinen Klassen wird, falls vorhanden, immer der Destruktor aufgerufen. Ich hab hier die PHP 5.2.1 Version.
 
Zurück
Oben