// der php hacker

// archiv

Workshop: Brawler – Alles im Rahmen, Teil 2

Geschrieben am 07. 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

Bevor ich die eigentliche Funktionalität der Software erarbeite, benötige ich eine Infrastruktur. Wie schon erwähnt wünsche ich das MVC-Pattern zu nutzen. Dazu brauche ich noch eine einfache und schnelle Möglichkeit Ausgaben in die Konsole zu schreiben bzw. Einnahmen anzunehmen. Zu allererst brauche ich jedoch einen Einstiegspunkt.

Das Programm

Damit brawler über die Konsole ausgeführt werden kann, ohne ein umstädliches „/usr/bin/php /path/to/brawler” eintippen zu müssen, erstelle ich mir eine ausführbare PHP-Datei. Ich nenne sie einfach nur „brawler“ – das sieht einfach schöner aus. Mit chmod +x brawler habe ich das ganze auch noch ausführbar gemacht. In der Datei an sich steht nicht viel:

#!/usr/bin/php
<?php
	include 'src/brawler.php';
?>

Wir sehen, diese Datei hat einzig und allein den Zweck, Brawler bequem aufrufen zu können.

Der Einstiegspunkt

Der eigentliche Einstieg findet erst in der inkludierten Datei statt. Als allererstes schraube ich das Error-Reporting hoch. Wir wollen ja auch nichts verpassen. Dann ergänze ich die Include-Pfade um meinen eigenen, hol mir meinen Autoloader und registriere diesen. Zuguterletzt wird die Applikation losgetreten:

	// turn on error messages
	error_reporting(E_ALL ^ E_STRICT);
	ini_set('display_errors', 1);	

	// set include path
	set_include_path(get_include_path(). PATH_SEPARATOR. dirname(__FILE__));

	// require autoloader
	require_once 'Brawler/Loader.php';

	// register autoloader
	spl_autoload_register(array('Brawler_Loader', 'load'));

	// kick application
	return Brawler_Application::run();

Auch hier wieder: Ist ja gar nicht so viel. Der Autoloader ist schnell erklärt: Er nimmt sich den Klassennamen, ersetzt die Unterstriche durch DIRECTORY_SEPARATOR, hängt ein „.php“ hinten dran und inkludiert die Datei. Autoloading auf die ganz billige Art :-)

Auch die Application-Klasse ist eher eine faule Natur. Sie instanziert den Front-Controller und lässt ihn dispatchen. Hier kommt dann also das MVC ins Spiel …

Vorhang auf für das Brawler Mini-MVC

Ok, nicht soviel klatschen, denn eigentlich ist es bisher mehr ein VC. Das M haben wir ja noch gar nicht. Aber auch mit Modellen wäre das Dingen immer noch sehr schlank. Liegt aber wahrscheinlich nicht zu letzt daran, dass ich erst einmal nur das geschrieben habe, was ich jetzt dringend brauchen werde. Neue Funktionalität wird erst bei Bedarf nachgerüstet.

Schauen wir uns die Controller-Klasse etwas genauer an:

	/**
	 * MVC controller class
	 *
	 * @package		Brawler
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	class Brawler_Controller {
		/**
		 * Attached view
		 *
		 * @var Brawler_View
		 */
		protected $_view = null;

		/**
		 * Defines wether the controller should render the view
		 *
		 * @var Boolean
		 */
		protected $_render = true;

		/**
		 * Dispatches a controller call
		 *
		 * @param String $action
		 * @return void
		 */
		public function dispatch($action = 'index') {
			if(method_exists($this, $action.'Action')) {
				call_user_func(array($this, $action.'Action'));
			} else {
				throw new Exception('Could not find action '. $action);
			}

			if($this->_render &amp;&amp; $this->_view) {
				$this->_view->render();
			}
		}

		/**
		 * Forwards to another controller and/or action
		 * @param String $controller
		 * @param String $action
		 * @return void
		 */
		public function forward($controller, $action = 'index') {
			$controllerClass =
				'Brawler_Controller_'.
				ucfirst(strtolower($controller));

			$controller = new $controllerClass;

			call_user_func(array($controller, 'dispatch'), $action);
		}

		/**
		 * Sets the actial view
		 *
		 * @param Brawler_View $view
		 * @return void
		 */
		public function setView(Brawler_View $view) {
			$this->_view = $view;
		}

		/**
		 * Returns the current view
		 *
		 * @return Brawler_View
		 */
		public function getView() {
			return $this->_view;
		}
	}

Vielleicht kommen einem die Namen der Methoden und Properties bekannt vor. Ich hab mich da etwas vom Zend Framework inspirieren lassen. Natürlich kann man das auch anders nennen, aber dann müsste ich mich ja auch noch umgewöhnen ;)

Wirklich interessant sind auch nur die Methoden dispatch und forward. Wird ein Controller angesteuert, wird die Dispatch-Methode aufgerufen. Diese sucht nach der gewünschten Action und ruft diese auf. Ist sie durchgelaufen, wird der View gerendert – wenn einer zugewiesen wurde.

Die forward Methode ermöglicht es im Grunde nur, einen anderen Controller mit ins Boot zu holen, und den Programmfluss dorthin zu leiten. Damit lässt es sich einfach bewerkstelligen, Bestimmte Aktionsfragmente die häufiger genutzt werden in Reihe zu schalten und somit Duplicate Code zu verhindern.

Der Front-Controller ist übrigens eine Ableitung der Controller-Klasse und hat nur eine Methode, die er überschreibt:

	/**
	 * MVC Front Controller
	 *
	 * @package		Brawler
	 * @subpackage	Controller
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	class Brawler_Controller_Front extends Brawler_Controller {
		/**
		 * (non-PHPdoc)
		 * @see trunk/src/Brawler/Brawler_Controller#dispatch()
		 */
		public function dispatch($action = 'index') {
			$this->forward('help');
		}
	}

Da wir noch keine Funktionalität haben, schickt der Front-Controller direkt weiter an den Help-Controller. An dieser Stelle werden im nächsten Schritt die ersten Observer andocken um den Programmfluß modular manipulieren zu können. Dazu aber mehr im dritten Teil.


	/**
	 * Help Controller
	 *
	 * @package		Brawler
	 * @subpackage	Help
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	class Brawler_Controller_Help extends Brawler_Controller {
		/**
		 * Index action
		 * @return void
		 */
		public function indexAction() {
			$this->setView(new Brawler_View_Help_Index());
		}
	}

Wie ihr seht, immer noch alles sehr schlank. Der Help-Controller macht nichts weiter als den Index-View zu laden. Den lasse ich mal weg, der besteht nämlich tatsächlich nur aus der Ausgabe den man beim starten des Programmes sieht. Stattdessen noch mal ein kleines Blick auf die Basis-View-Klasse:

	/**
	 * MVC View class
	 *
	 * @package		Brawler
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	class Brawler_View {
		/**
		 * Assignes values
		 *
		 * @var Array
		 */
		protected $_values = array();

		/**
		 * Magic setter
		 *
		 * @param String $name
		 * @param Mixed $value
		 * @return void
		 */
		public function __set($name, $value) {
			$this->_values[$name] = $value;
		}

		/**
		 * Magic getter
		 *
		 * @param String $name
		 * @return Mixed
		 */
		public function __get($name) {
			if(isset($this->_values[$name])) {
				return $this->_values[$name];
			} else {
				return null;
			}
		}

		/**
		 * Renders the view
		 *
		 * @return void
		 */
		public function render() {
			ob_start();
			$this->view();
			$output = ob_get_contents();
			ob_end_clean();

			Brawler_Console_Output::output($output);
		}
	}

Jeweils ein magischer Getter und Setter, damit können die Werte, die im View verbraten werden aufgerufen werden. Die Methode render sorgt dafür, dass alles ausgewertet ausgegeben wird – und wird in der Regel auch nur vom Front-Controller aufgerufen.

Die kleine Kommandozeile

Damit ich bequem Ausgaben (auch an das Error-Terminal) tätigen & Eingaben entgegen nehmen kann und eine kleine Parameterauswertung habe, war der nächste Schritt eine Sammlung von simplen Klassen die das für mich bewerkstelligen.

Die Hauptklasse ist später dafür da, Argumente abzufragen.


	/**
	 * Provides basic cli application features
	 *
	 * @package		Brawler
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	class Brawler_Console {
		/**
		 * Holds the parsed arguments
		 *
		 * @var Brawler_Console_Argument_List
		 */
		protected static $_arguments = null;

		/**
		 * Returns a single argument
		 *
		 * @param String $argument
		 * @return Brawler_Console_Argument
		 */
		public static function getArgument($argument) {
			if(!self::$_arguments) {
				self::_parseArguments();
			}

			$i = self::$_arguments->getIterator();
			while($i->valid()) {
				if($i->current()->getName() == $argument) {
					return $i->current();
				}
				$i->next();
			}

			return null;
		}

		/**
		 * Returns all arguments
		 *
		 * @return Brawler_Console_Argument_List
		 */
		public static function getArguments() {
			if(!self::$_arguments) {
				self::_parseArguments();
			}

			return self::$_arguments;
		}

		/**
		 * Parses the given arguments
		 *
		 * @return void
		 */
		public static function _parseArguments() {
			self::$_arguments = new Brawler_Console_Argument_List();

			$args = new ArrayObject($_SERVER['argv']);
			$args->offsetUnset(0);
			$i = $args->getIterator();
			while($i->valid()) {
				if(substr($i->current(), 0, 1) == '-') {
					// parse argument
					self::_parseArgument($i->current());
				} else {
					// invalid call
					throw new Exception('Invalid call');
				}

				$i->next();
			}
		}

		/**
		 * Parses a single argument
		 *
		 * @param $argument
		 * @return void
		 */
		protected static function _parseArgument($argument) {
			if(strstr($argument, '=')) {
				// definition
				$name = substr($argument, 1, 1);

				$parts = split('=', $argument);
				if(substr($parts[1], 0, 1) == '"') {
					$value = substr($parts[1], 1, strlen($parts[1] - 2));
				} else {
					$value = $parts[1];
				}

				self::$_arguments->append(new Brawler_Console_Argument($name, $value));
			} else {
				// option or optiongroup
				for($i = 1; $i < strlen($argument); $i++) { 					self::$_arguments->append(new Brawler_Console_Argument(substr($argument, $i, 1)));
				}
			}
		}
	}

Neben Sets von Aufrufparametern, lassen sich auch einzelne benannte anfordern. Wir sehen hier auch, dass sobald ein Wert angefordert wird, prüft die Klasse, ob sie schon Argumente hat. Ist das nicht der Fall, werden die übergebenen geparst.

Die Klasse nimmt Argumente in drei Varianten an:

  1. Einzelne Argumente, jeweils mit einem Minus-Zeichen vor dem Buchstaben
  2. Argumentgruppen, mit einem Minuszeichen vor dem ersten Buchstaben. Alle weiteren Argumente folgen ohne Whitespace dazwischen
  3. Zuweisungen, bei denen das Argument einzeln angegeben wird, gefolgt von einem Leerzeichen und dem Wert. Enthält der Wert Whitespace oder Zeichen, die als weiteres Argument gewertet werden könnten, kann dieser mit Anführungszeichen eingefasst werden.

Um Ausgaben zu machen, kann man sich der Klasse Brawler_Console_Output bedienen.


	/**
	 * Provides output features
	 *
	 * @package		Brawler
	 * @subpackage	Console
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	class Brawler_Console_Output {
		protected static $_stdout;
		protected static $_stderr;

		protected static function _init() {
			self::$_stdout = fopen('php://stdout', 'w');
			self::$_stderr = fopen('php://stderr', 'w');
		}

		public static function output($msg, $error = false) {
			if(!self::$_stdout OR !self::$_stderr) {
				self::_init();
			}

			if($error) {
				$stream = self::$_stderr;
			} else {
				$stream = self::$_stdout;
			}

			fwrite($stream, $msg.PHP_EOL);
		}
	}

Wir sehen, die Klasse initialisiert beim ersten Versuch einer Ausgabe den Standard- und Error-Output-Stream. Über einen optionalen Parameter kann ich Ausgaben nun an das Error-Terminal schicken.

Die Input-Klasse Brawler_Console_Input ist lediglich abstrakt. Warum? Ganz einfach: Im Gegensatz zur Ausgabe fallen mir bereits zu beginn mehrere verschiedene Anwendungsfälle ein. Wir wollen lediglich eine Zeile entgegen nehmen, wir wollen eine Ja-Nein-Abfrage machen, die Eingabe muss einem bestimmten Format entsprechen und vieles mehr. Dahingehend sieht die Abstrakte Klasse so aus:


	/**
	 * Abstract Input
	 *
	 * @package		Brawler
	 * @subpackage	Console
	 * @author 		Cem Derin
	 * @copyright	2009 Cem Derin,
	 */
	abstract class Brawler_Console_Input {
		/**
		 * Retrieves input
		 *
		 * @param $msg Query message
		 * @param $stripEnter Strip trailing Linebreak
		 * @return String
		 */
		public static function get($msg, $stripEnter = true) {
			Brawler_Console_Output::output($msg);

			do {
				$return = fgets(self::_getStream());
			} while(!self::_validate($return));

			if($stripEnter) {
				$return = substr($return, 0, strlen($return) - 1);
			}

			return $return;
		}

		/**
		 * Returns the input stream
		 *
		 * @return Resource
		 */
		protected static function _getStream() {
			return fopen('php://stdin', 'w');
		}

		/**
		 * Validates the user input
		 *
		 * @param $value
		 * @return Boolean
		 */
		protected static function _validate($value) {
			return true;
		}
	}

Wollen wir nun eine besondere Art der Benutzerabfrage bauen, reicht es in der Regel, wenn wir diese Abstrakte Klasse ableiten und die Methode _validate überladen.

Was kommt als nächstes?

Das grundlegende Gerüst steht. Was nun fehlt – und als nächstes kommt – ist die Implementierung einer leichten aber umfassenden Plugin-Lösung. Jeder ist herzlich eingeladen sich da zu versuchen. Parallel dazu werde ich eine eigene Lösung erarbeiten und im nächsten Artikel präsentieren. Den Code gibt es wie schon erwähnt auf der Google Code Seite: http://code.google.com/p/brawler/

Ich freue mich auf euer Feedback!


#001
07. Dez 2009
uli

kleine anmerkung:
error_reporting(E_ALL ^ E_STRICT);
XOR ist sicherlich nicht was du willst, da hier E_STRICT abgeschaltet wird, wenn es in der config zuvor an war und nur an, wenn es zuvor aus war. Ich nehme an du meinst eher:
error_reporting(E_ALL | E_STRICT);


#002
07. Dez 2009
Cem Derin

Recht hast du. Da hab ich nur an meine Konfiguration gedacht. Vielen Dank!

#003
08. Dez 2009

[...] Workshop: Brawler – Alles im Rahmen, Teil 2 [...]


#004
08. Dez 2009

Wieso iterierst du nicht direkt mit foreach über einen Iterator und nutzt stattdessen die recht unübersichtliche while-Schreibweise?


#005
08. Dez 2009
Cem Derin

Gute Frage … habe das irgendwann mal angefangen und mich wohl schon zu sehr dran gewöhnt. Aber ich kann dir ja schon mal verraten, dass ich in kurzen Abständen refactorn werde – da nehm ich das direkt mal mit auf die Agenda ;)

Danke für das Feedback übrigens. Mir ist ja schon klar, dass das Thema wahrscheinlich kaum einen Interessiert :)


#006
08. Dez 2009

Das mich das Thema nicht interessiert, habe ich nicht gesagt :) Finde den MVC-Ansatz für die Konsole recht spannend.


#007
08. Dez 2009
Cem Derin

Jetzt fällt mir auch wieder ein, warum ich den Weg über den Iterator gehe: So kann ich Code-Completion in meiner IDE nutzen ;)

#008
09. Dez 2009

[...] Workshop: Brawler – Alles im Rahmen, Teil 2 [...]


#009
09. Dez 2009

Ok, gutes Argument. Falls du das Zend Studio nutzt, da ist so etwas möglich, um Variablen zu typisieren:
/*@var $myVar MyClass_Blubb*/

Dann kann dir deine IDE trotz Iterator helfen :)


#010
09. Dez 2009
Cem Derin

Zwar nicht Zend Studio, aber Eclipse, und da geht das auch. Allerdings muss ich gestehen, dass mir das persönlich zu umständlich ist, immer drüber zu setzen. Natürlich könnte man nun argumentieren, dass es auch Aufwand ist, das while-Beiwerk zu bauen … aber was soll ich sagen, so gehts mir schneller von bzw. aus der Hand ;)


#011
10. Dez 2009
onemorenerd

Wieso arbeitest du in Brawler_Controller::forward() mit call_user_func?


#012
10. Dez 2009
Cem Derin

Weil ich zu faul war, nachzuschlagen, wie ich das über reflection löse – denn produktiv hab ich das noch nie gemacht

Edit: Verlesen. Kann ich dir sagen, das hatte ich vorher noch einmal umgebaut und hab den Inhalt der Methode kopiert und schnell bearbeitet. Ist an der Stelle recht sinnbefreit, richtig. Fliegt beim Refactoring demnächst raus!

// 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.