// der php hacker

// archiv

Workshop: Brawler – Der Rutengänger, Teil 4

Geschrieben am 09. Dez 2009 von Cem Derin

Zuvor:

  1. Workshop: Brawler – The Web Application Security Scanner, Teil 0
  2. Workshop: Brawler – Eine Frage der Lizenz, Teil 1
  3. Workshop: Brawler – Alles im Rahmen, Teil 2
  4. Workshop: Brawler – Not unplugged, Teil 3

Nachdem ich mir eine kleine MVC-Architektur sowie diverse Tools zum ahandeln der Konsole geschrieben habe und im letzten Teil auch noch die Grundlegende Plugin-Funktionalität implementieren konnte, werde ich heute einen Router in den MVC-Part einbauen – der auch direkt die Plugins berücksichtigt. Ganz zum Schluss gibt es dann auch noch mal einen flüchtigen Blick auf das Chaos das ich „Arbeitsweise und Stil“ nenne ;)

Wie wird geroutet?

In der schönen und heilen Webapplikationswelt, gestalten sich Routen recht „einfach“. Die Route und der Call sind im Grunde so gut wie immer identisch bzw. die Ableitung ist ersichtlich. Zum Beispiel ist so gut wie klar, dass der Call „http://example.com/user/list“ auf den User-Controller und die List-Action routet.

Auf der Kommandozeile sieht das ein bisschen anders aus. Hier übergibt man mehr oder weniger nur Flags bzw. Flag-Wert-Paare und die Applikation muss mit diesen Informationen routen.

Also, wie habe ich das angestellt. Schauen wir uns die Router-Klasse an:

<?php
	/**
	 * Router class
	 *
	 * @package     Brawler
	 * @author      Cem Derin,
	 * @copyright   2009 Cem Derin,
	 */
	class Brawler_Router {
		/**
		 * Holds the registered routes
		 *
		 * @var Brawler_Router_Route_List
		 */
		protected static $_routes = null;

		/**
		 * Registers a given route
		 *
		 * @param Brawler_Router_Route $route
		 * @return void
		 */
		public static function registerRoute(Brawler_Router_Route $route) {
			if(!self::$_routes) {
				self::_initRouteArray();
			}

			self::$_routes->append($route);
		}

		/**
		 * Inits the Route List
		 *
		 * @return void
		 */
		protected static function _initRouteArray() {
			self::$_routes = new ArrayObject();
		}

		/**
		 * More or less the dispatcher ;)
		 *
		 * @return void
		 */
		public static function route() {
			$i = self::$_routes->getIterator();
			while($i->valid()) {
				if(!self::_checkRoute($i->current())) {
					break;
				}
				$i->next();
			}
		}

		/**
		 * Checks a route for matching conditions
		 *
		 * @param Brawler_Router_Route $route
		 * @return Bool
		 */
		protected static function _checkRoute(Brawler_Router_Route $route) {
			// check arguments
			$argumentIterator = $route->getArguments()->getIterator();
			$match = true;
			while($argumentIterator->valid()) {
				if(!Brawler_Console::getArgument($argumentIterator->current()->getFlag())) {
					// flag is missing break;
					$match = false;
					break;
				}

				if($argumentIterator->current()->getOnValue()) {
					if(!Brawler_Console::getArgument($argumentIterator->current()->getFlag())->getValue()) {
						// value is missing, break
						$match = false;
						break;
					}

					if($argumentIterator->current()->getSpecificValue()) {
						$specValue = $argumentIterator->current()->getSpecificValue();
						if(Brawler_Console::getArgument($argumentIterator->current()->getFlag())->getValue() != $specValue) {
							// value does not match, break
							$match = false;
							break;
						}
					}
				}
				$argumentIterator->next();
			}

			// dispatch
			if($match) {
				$route->getController()->dispatch($route->getAction());
			} else {
				return true;
			}

			// check chain
			return $route->getChain();
		}
	}

An Properties hält der Router nicht viel vor – lediglich eine Liste der Routen die man über die Methode registerRoute registrieren kann. Anhand dieser Routen wird der Router später dispatchen können (und ich habe ihn damit mehr oder weniger in den Stand eines Dispatcher erhoben).

Wird der Router angewiesen, seiner eigentlichen Arbeit nachzugehen, so durchläuft er alle Routen, und wenn die gewünschten Parameter oder Parameter-Wert-Paare passen, wird der hinterlegte Controller mit der hinterlegten Action aufgerufen. Da man Routen auch in Reihe schalten kann, gibt es das Attribut chain. Damit kann die Route bestimmten, ob nach ihr noch weitere Routen nachgegangen werden darf, die auf den Call passen.

<?php
	/**
	 * Represents a single route
	 *
	 * @package     Brawler
	 * @subpackage  Router
	 * @author      Cem Derin,
	 * @copyright   2009 Cem Derin,
	 */
	class Brawler_Router_Route {
		/**
		 * Name of this route
		 *
		 * @var String
		 */
		protected $_name = null;

		/**
		 * Arguments for this route
		 *
		 * @var Brawler_Router_Argument_List
		 */
		protected $_arguments = null;

		/**
		 * Controller to call
		 *
		 * @var Brawler_Controller
		 */
		protected $_controller = null;

		/**
		 * Action to call
		 *
		 * @var String
		 */
		protected $_action = 'index';

		/**
		 * Defines whether the route can dispatch in chain
		 *
		 * @var Bool
		 */
		protected $_chain = null;

		/**
		 * Ctor
		 *
		 * @param String $name
		 * @param Brawler_Router_Argument_List $arguments
		 * @param Brawler_Controller $controller
		 * @param String $action
		 * @return void
		 */
		public function __construct($name, Brawler_Router_Argument_List $arguments, Brawler_Controller $controller = null, $action = null, $chain = true) {
			$this->_name = $name;
			$this->_arguments = $arguments;

			if($controller) {
				$this->_controller = $controller;
			} else {
				$this->_controller = new Brawler_Controller_Index();
			}

			if($action) {
				$this->_action = $action;
			}

			$this->_chain = $chain;
		}

		/**
		 * Returns the arguments attached to this route
		 *
		 * @return Brawler_Router_Argument_List
		 */
		public function getArguments() {
			return $this->_arguments;
		}

		public function getChain() {
			return $this->_chain;
		}

		public function getController() {
			return $this->_controller;
		}

		public function getAction() {
			return $this->_action;
		}
	}

Eine Route baut sich aus einem Namen (den brauchen wir eigentlich noch nicht, aber man weiß ja nie), einer Liste von Argumenten, einem Controller, eine Action und der Angabe ob die Route in Reihe geschaltet werden darf auf. Bis auf die Argument-Liste und den Namen ist alles optional … wobei es natürlich wenig sinnvoll ist, wenn alle Routen auf den Index-Controller zeigen ;)

Eine kleine Besonderheit an dieser Stelle ist, dass direkt eine Instanz des Controller übergeben wird. Man könnte stattdessen auch den Klassennamen übergeben, aber irgendwie erschien mir das an dieser Stelle unpassend.

Die Argumente, die eine Route braucht, unterscheiden sich ein wenig von denen, die Plugins nennen, wenn sie gefragt werden, welche sie überhaupt auswerten. Das liegt zum einen daran, dass hier gar nicht genau diese auftauchen müssen, zum anderen, dass völlig andere Informationen notwendig sind. Schauen wir uns die Klasse an:

<?php
	/**
	 * Represents an argument for routes
	 *
	 * @package     Brawler
	 * @subpackage  Router
	 * @author      Cem Derin,
	 * @copyright   2009 Cem Derin,
	 */
	class Brawler_Router_Argument {
		/**
		 * The flag this route is for
		 *
		 * @var String
		 */
		protected $_flag = null;

		/**
		 * Defines whether this route only is for vakued flags
		 *
		 * @var Bool
		 */
		protected $_onValue = null;

		/**
		 * Defines whether this route is only for a valued flag with a specific value
		 *
		 * @var String
		 */
		protected $_specificValue = null;

		public function __construct($flag, $onValue = false, $specificValue = null) {
			$this->_flag = $flag;
			$this->_onValue = $onValue;
			$this->_specificValue = $specificValue;
		}

		/**
		 * Returns the setted flag
		 *
		 * @return String
		 */
		public function getFlag() {
			return $this->_flag;
		}

		/**
		 * Returns whether this argument needs a value
		 *
		 * @return Boolean
		 */
		public function getOnValue() {
			return $this->_onValue;
		}

		/**
		 * Returns whether a specific value is needed
		 *
		 * @return String
		 */
		public function getSpecificValue() {
			return $this->_specificValue;
		}
	}

Auch nichts wildes! Der Konstruktor will das Flag, auf das die Route reagieren soll, fragt noch, ob es einen Inhalt haben muss und wenn ja, ob dieser einem bestimmten Wert entsprechen soll (für so Sachen wie „m=low/m=med/m=hi“ und so). Das war es auch schon.

Der Router wertet den Krempel aus, ob alle Faktoren der Route auf den Call matchen und – wenn ja – dispatcht die Klasse. Aus die Maus. Eigentlich ganz einfach, oder?

Und da mir von vorne herein klar war, dass ich diesmal länger am Code als an der Erläuterung sitze, gibt es heute noch die Eingangs erwähnte Einführung in meine kleine Hacker-Welt

Sauber, schlank, schnell, robust

Das sollte Code sein. Meistens kann man ihn auch vom Kopf in die Tastatur bekommen, und er entspricht genau den Kriterien. Manchmal – aus Faulheit, aus Unwissen oder weil dort ein Hund mit hochstehendem Schwanz lang läuft) prügelt man aber auch eine solche Grütze in den Rechner, dass man sich beim nächsten mal ernsthaft fragt, ob nicht doch LSD im Trinkwasser ist.

Bei mir setzt dieses Phänomen mit steigender Müdigkeit ein (übrigens ein Grund, warum nächtelanges durchhacken eher ein romantisches Nerd-Klischee ist und in den seltensten Fällen dabei Code entsteht, der obigen Ansprüchen genügt), und da ich das Zeug hier vorwiegend Abends schreibe und ohnehin schon den ganzen Tag programmiert habe, ist dieses Projekt hier besonders anfällig!

Nichtsdestotrotz möchte ich meine Ansprüche nicht herunterschrauben. Daher werde ich meinen eigenen Code in kurzen Abständen einem Review unterziehen, und jedes Fragment kritisch beurteilen sowie im Bedarf einem Refactoring unterziehen. Das wird dann auch Teil meines Workshops werden (und glaubt mir, umfangreiches Refactoring macht mit PHP keinen Spaß ;) ).

Dokumentation ist alles

Ich persönlich hasse es, wenn Code undokumentiert ist. Daher nutze ich die Doc-Syntax. In der Regel finde ich, dass jede Klasse (bzw. jede Datei) einen Kopf braucht, in dem Angegeben wird, wer den Mist verbrochen hat, wer dannach daran rumgepfuscht hat. Für Methoden halte ich das etwas laxer. Da gilt: Steht da nix, war es der Klassen(haupt)autor.

Die Angabe von Packages und Subpackages lässt die Dokumentation später aufgeräumt erscheinen. Und eine Erklärung sowie eine Typvorgabe bei Parametern und Rückhaben ist unerlässlich!

Danke für das erste Feedback

Bedanken möchte ich mich im 4. (eigentlich ja 5.) Teil des Workshops bei Uli und Martin, die mich auf zwei kleine Unschönheiten hingewiesen haben. Mehr davon, bitte! ;)

Und jetze?

Im nächsten Teil werde ich die Hooks für die Plugins integrieren sowie den ersten spannenden Teil bauen: Das scannen eines Hosts. Und da kommen dann auch schon die ersten Fallstricke ;)


#001
09. Dez 2009

Einen ersten Punkt für ein Refactoring hätte ich bereits. Und zwar sind das die inzwischen schon recht vielen statischen Methoden. Das fängt schon bei der Application-Klasse an. Gerade die Output- und Input-Klassen würde ich zwecks Vererbung auf Objektmethoden umschreiben. Die Application-Instanz verfügt dann über jeweils eine Instanz der beiden Klassen. Ähnlich verhält es sich mit der Console-Klasse und dem PluginLoader.


#002
09. Dez 2009
Cem Derin

Ich hatte es schon getwittert: Ich habe an den Stellen erst einmal auf statische Methoden gesetzt, weil ich zu faul für Singletons war … kann Instanziert werden, muss ich die jeweils gültigen Objekte vorhalten … das wollte ich beim prototyp erst einmal nicht. Ansonsten guter Punkt. Ist aufgenommen. Andernfalls kannst du es ja auch gerne selbst machen =P


#003
10. Dez 2009
Sebastian

Hallo Cem,

ich bin eben erst auf deinen Blog gestoßen (durch Nils – du musst ihm jetzt dankbar sein^^), auf jeden Fall ist mir aufgefallen, dass du an Brawler ja erst ein paar Tage schreibst. Wie viel Zeit investierst du da jeden Tag?

Gruß Sebastian

#004
15. Dez 2009

[...] Workshop: Brawler – Der Rutengänger, Teil 4 [...]

// kommentieren

// senden
theme von mir, software von wordpress, grid von 960 grid system. funktioniert in allen browsern, aber der safari bekommt das mit der schrift am schönsten hin.