Kleine SEO-Tools mit Quelltext in PHP

ToolsEs gibt viele Sites, die online Tools für die Suchmaschinenoptimierung (SEO) anbieten. Es gibt Handverlesene Verzeichnisse von Linkvendor, Eigenentwicklungen von Ravenseo und riesige Zusammenstellungen von den Fachinformatikern, dem Website Spy, dem SEO-Chat und der SEO-Company. Wieso braucht es denn noch mehr? Ganz einfach: Google Schweiz wird – extrem überraschenderweise – nirgends so richtig berücksichtigt. Ausserdem will ich auf der Kommandozeile arbeiten und den Quellcode möchte ich auch, um die Bewertungen nachzuvollziehen und um darin herumzuwurschteln.

Naja, dann hab ich halt mal angefangen. Das Resultat gibts auch zum Download: PHP SEO-Tools.
Weiterlesen

Active Record Pattern in PHP

StorageIn PHP gibt es verschiedene Implementierungen des Active Record Patterns. Auch wenn es uns nimmer so auffällt, die objektorientierte Welt des Codes und die relationale Welt der Daten sind zwei verschiedene Paradigmen, und es braucht unheimlich viel Leim um sie zusammenzukleben. Objektorientierte Datenbanken entwickeln sich, sind jedoch noch nicht Standard (um es mal vorsichtig auszudrücken). Das Active Record Pattern hilft uns mit ein Bisschen Standard-Klebe.

Mit diesem Pattern können Daten aus der Datenbank gesucht und direkt in Objekte übergeführt werden. Änderungen an den Objekten und das Speichern bewirken, dass diese Änderungen auch in die Datenbank übernommen werden.

Ich habe mich für MyActiveRecord von Jake Grimley entschieden, weil es sauber implementiert, nicht zu gross und trotzdem nicht featurearm ist. Die Dokumentation ist ziemlich gut und reicht aus um zu starten.

Benutzung von MyActiveRecord

Suchen und finden

Der Dreh- und Angelpunkt dürfte die Funktion FindAll($class, $where=0, $orderby='id ASC', $limit=10000, $offset=0) sein. Sie liefert eine Kollektion von Objekten der entsprechenden Klasse zurück:

foreach (MyActiveRecord :: FindAll('thing') as $thing) {
  echo "I am a {$thing->name}!<br />\n";
}

Weiterlesen

Drupal mit neuen Google AdSense Werbeblöcken (Serverbasiert)

Neues AdSense


(Affilliate Link 🙂 ) sind neuerdings Serverbasiert. Das heisst: Aussehen, Inhalt, etc kann grösstenteils von der AdSense-Seite aus gemanaged und muss nicht mehr mühsam von Hand auf jeder Seite eingepflegt, geftpt und gecheckt werden. Das geniale Drupal AdSense Modul beherrscht diese neue Variante leider (noch) nicht von Haus aus. Mit einem kleinen Patch geht das aber.

Vorgehen

  1. Die Datei adsense.module mit diesem Patch so patchen, wie in diesem Beitrag beschrieben.
  2. Auf der Google AdSense Seite AdSense-Setup → Anzeigen Verwalten aufrufen und eine neue Anzeige designen.
  3. Den Code anzeigen lassen und die Slot-Nummer notieren. Es sollte etwas sein wie 2666876213.
  4. In Dupal als Administrator Administer → Site configuration → Google AdSense aufrufen und Custom Channels aufklappen.
  5. Die Slotnummer in einen Channel schreiben.
  6. Diesen Channel beim Block für die Anzeige einsetzen.
  7. Warten… Und sich freuen…

Einfache Animationen auf der Konsole mit PHP (1-Dimensional)

AnimationWenn man mit PHP Konsolenapplikationen schreibt, wäre es manchmal schön, eine pfundige Ausgabe mit etwas Bewegung erzeugen zu können. Solche Kleinstanimationen eignen sich auch für Aufgaben beim Erlernen von PHP, denn sie sind sehr unaufwändig und es tut sich trotzdem was.

Diese Ausgaben sind Eindimensional (auf einer Linie) und funktionieren unter Linux und Windows. Ich warte sehnsüchtig auf die PHP-Languagebindings für die aalib oder libcaca :-)…

Der Trick ist der Carriage Return (CR), der den Cursor an den Anfang der Zeile setzt, aber nicht vertikal weiterscrollt. in PHP wird dieses Zeichen mit \r codiert. Grundsätzlich ist der Ablauf wie folgt:

Bilderfolge in ein Array rendern
Solange man Lust hat:
    Zeige nächstes Bild
    Warte
  }
}

Unter Linux können zur weiteren Effektsteigerungen auch noch ANSI-Escapesequenzen verwendet werden. Mit diesen kryptischen Dingern kann man den einzelnen Zeichen Vorder- und Hintergrundfarbe verpassen und verwirrendes einschalten wie etwa ein Blinken. ESCAPE ist in PHP beispielsweise 33 in oktal und wird mittels \033 dargestellt.

Nun, zeig mal Code!

<?php
// Render Movie
$frames=array();
// 10 images for this movie
for($i=0;$i<10;$i++) {
    // The background consits of blue underscores
    $frame=array_fill(0, 10, "\033[34m_\033[0m");
    // One actor is a red star
    $frame[$i]="\033[31m*\033[0m";
    // The other a green dot
    $frame[9-$i]="\033[32m.\033[0m";
    // Join Background and actors
    $frames[]=join("",$frame);
}

// Output
for($i=0;$i<20;$i++) {
  foreach($frames as $frame) {
        // Rewind cursor and show frame
        print "\r".$frame;
        // Sleep a bit, because nights are hard these times...
        usleep(100000);
  }
}
print "\n";
?>

Erstellen eines Timers in Xoops

Xoops ist ein geniales CMS, auch wenn es in letzter Zeit durch administrative Querelen nicht sehr gute Schlagzeilen gemacht hat. Wir benutzen es in einer erweiterten Version als Intranet.

Nun braucht ein Modul plötzlich unendlich lange um sich darzustellen. Das hindert unseren rasanten Arbeitsfluss und darum muss darum korrigiert werden. Statt rumzufrickeln lohnt es sich, die Sache seriös anzugehen.

Xoops bietet eine sehr gute Debugfunktion an: Adminsitration → Systemeinstellungen → Voreinstellungen → Allgemeine Einstellungen → Debug-Modus kann auf PHP-Debug gesetzt werden. Danach werden unten an der Seite viele Infos dargestellt, unter Anderem SQL-Befehle und Timer.

Xoops Debug

Genau solche Timer kann man selber einfach erstellen. Zuerst muss der XoopsLogger eingebunden werden:

include_once(XOOPS_ROOT_PATH . '/class/logger.php');

Um einen Timer zu starten:

$xoopsLogger->startTime( 'absence_count_query' );

Um ihn wieder zu stoppen:

$xoopsLogger->stopTime( 'absence_count_query' );

Diese Timer wird nun – wie oben ersichtlich – wunderschön in die Debugfunktion eingereiht.

Eine eigene Programmiersprache erschaffen? Lexer und Parser in PHP!

Ich will Gott sein!

Paper SnippetsWer träumt nicht davon: Eine eigene Programmiersprache, denn der Syntax und das Wort sind Ausdruck! Einen Schritt weiter gehen und vom Programmiersprachenanwender zum Programmiersprachenerschaffer aufsteigen! Auch dies ist nun möglich in PHP, doch leider nur mit minimaler Dokumentation.

Ich gehe davon aus, dass die Leserin/Der Leser Grundlagen im Compilerbau besitzt. Ansonsten müsste man sich diese noch aneignen. Compilerbau ist rekursiv und somit göttlich! *schwärm*.

Das Beispiel

Ich möchte einen Interpreter bauen für normale, arithmetische Ausdrücke wie 9*(8+6)-2/(-4). Klar könnte man diesen einfach durch eval hauen, aber erstens macht das nicht so viel Spass und zweitens ist dieses Beispiel sowas wie das Hello World des Compilerbaus.

Als BNF habe ich folgendes vor:

<Expression> ::= '+' <Expression> // Unäres + (Vorzeichen)
<Expression> ::= '-' <Expression> // Unäres - (Vorzeichen)
<Expression> ::= <Term>
<Expression> ::= <Expression> '+' <Term>
<Expression> ::= <Expression> '-' <Term>
<Term> ::=  <Factor>
<Term> ::= <Term> '*' <Factor>
<Term> ::= <Term> '/' <Factor>
<Factor> ::=  NUMBER // Ok, hier hab ich abgekürzt ;) Soll der Lexer erledigen...
<Factor> ::= '(' <Expression> ')'

Ein Problem stellen die unären Plus- und Minusoperatoren dar. Sie besitzen das gleiche Symbol, nicht aber die gleiche Priorität wie ihre binären Kollegen. Lemon erlaubt keine änderung der Priorität nach Regeln und so funzen jetzt halt auch Konstrukte wie —+++—+1…

Die Werkzeuge

Ich verwende PHP_LexerGenerator, der kompatibel zum re2c Format ist und den PHP_ParserGenerator der in grossen Teilen dem LEMON Parser Generator entspricht. Heruntergeladen werden können sie mittels folgenden Zeilen, die allerdings unter Umständen noch eine Anpassungen der Versionsnummer brauchen:

pear install channel://pear.php.net/PHP_ParserGenerator-0.1.5
pear install channel://pear.php.net/PHP_LexerGenerator-0.3.4

Als einziges, weiteres Beispiel für diese Libs ausserhalb der Doku habe ich nur dieses Drupalmodul gefunden.

Die Realisierung

Der Lexer

Für den Lexer muss eine plex Datei erstellt werden. Sie beinhaltet den Rumpf des Lexerobjekts und die lexikalischen Regeln:

TermLexer.plex

<?php
class TermLexer
{
    private $data;
    public $counter;
    public $token;
    public $value;
    public $node;
    public $line;
    private $state = 1;

    function __construct($data)
    {
        $this->data = $data;
        $this->counter = 0;
        $this->line = 1;
    }

/*!lex2php
%input $this->data
%counter $this->counter
%token $this->token
%value $this->value
%line $this->line
number = /[0-9]+(\.[0-9]+)?/
multiplication = /\*/

division = @/@
plus = /\+/
minus = /\-/
openP = /\(/
closeP = /\)/
other = /./
*/
/*!lex2php
%statename START
multiplication {
  $this->token = TermParser::TP_MULTIPLICATION;
  //echo "multiplication: ".$this->value."\n";
}
division {
  $this->token = TermParser::TP_DIVISION;
  //echo "division: ".$this->value."\n";
}
plus{
  $this->token = TermParser::TP_PLUS;
  //echo "plus: ".$this->value."\n";
}
minus{
  $this->token = TermParser::TP_MINUS;
  //echo "plus: ".$this->value."\n";
}
openP {
  $this->token = TermParser::TP_OPENP;
  //echo "openP: ".$this->value."\n";
}
closeP {
  $this->token = TermParser::TP_CLOSEP;
  //echo "closeP: ".$this->value."\n";
}
number {
  $this->token = TermParser::TP_NUMBER;
  //echo "number: ".$this->value."\n";
}
other {
  return false;
}
*/

}

So richtig laufen tuts aber noch nicht, da er Konstanten des Parsers benötigt. Die stehen erst zur Verfügung wenn wir den Parser auch gebaut haben.

Der Parser

Der Parser wird vom Lexer mit Tokens gefüttert. Die Notation im Lemon-Style unterscheidet sich von Lex und Yacc, ist aber durchaus logisch. Hier der ganze Code, mal hingeknallt:

TermParser.y

/* This is an example for a Parser in PHP */
%name TP_
%declare_class {class TermParser}
%include_class
{
    // states whether the parse was successful or not
    public $successful = true;
    public $retvalue = 0;
    private $lex;
    private $internalError = false;

    function __construct($lex) {
        $this->lex = $lex;
    }
}

%token_prefix TP_

%parse_accept
{
    $this->successful = !$this->internalError;
    $this->internalError = false;
    $this->retvalue = $this->_retvalue;
    echo "WORKED!!\n\n";
}

%syntax_error
{
    $this->internalError = true;
    echo "Syntax Error on line " . $this->lex->line . ": token '" .
        $this->lex->value . "' count ".$this->lex->counter." while parsing rule: ";
    foreach ($this->yystack as $entry) {
        echo $this->tokenName($entry->major) . '->';
    }
    foreach ($this->yy_get_expected_tokens($yymajor) as $token) {
        $expect[] = self::$yyTokenName[$token];
    }
    echo "\n"; 
    throw new Exception('Unexpected ' . $this->tokenName($yymajor) . '(' . $TOKEN. '), expected one of: ' . implode(',', $expect));
}

%left PLUS MINUS.
%left MULTIPLICATION DIVISION.

start(res)       ::= expression(expr). { res = expr; }

/* Unary minus or plus */
expression(res)  ::= PLUS expression(e). { res = +e; }
expression(res)  ::= MINUS expression(e). { res = -e; }

/* The common stuff */
expression(res)  ::= term(t). { res = t; }
expression(res)  ::= expression(e1) PLUS term(t2). { res = e1+t2; }
expression(res)  ::= expression(e1) MINUS term(t2). { res = e1-t2; }

term(res)        ::= factor(f). { res = f; }
term(res)        ::= term(t1) MULTIPLICATION factor(f2). { res = t1*f2; }
term(res)        ::= term(t1) DIVISION factor(f2). { res = t1/f2; }

factor(res)      ::= NUMBER(n). { res = n; }
factor(res)      ::= OPENP expression(e) CLOSEP. { res = e; }

Generieren der eigentlichen Parser und Lexer

Damit Parser und Lexer generiert werden und Terme gefüttert werden können, sei hier ein kleines Beispiel gegeben:

<?php
///////
// Parser and lexer have to be created only once! They create two files you can use later
//////
// Create Parser
passthru('php /usr/share/php/PHP/ParserGenerator/cli.php TermParser.y');

// Create Lexer
require_once 'PHP/LexerGenerator.php';
$lex = new PHP_LexerGenerator('TermLexer.plex');

# Test
// These are the created files
include_once("TermParser.php");
include_once("TermLexer.php");

$teststr='9*(8+6)-2/(-4)';

$lex = new TermLexer($teststr);
// $lex = new TermLexer(file_get_contents($lexerfile));

echo "-------------------------------\n";
echo " Parsing $teststr\n";
echo "-------------------------------\n";
$parser = new TermParser($lex);
while ($lex->yylex())  {
    echo "Parsing  {$lex->token} Token {$lex->value} \n";
    $parser->doParse($lex->token, $lex->value);
}
$parser->doParse(0, 0);

print "Returnvalue: ".$parser->retvalue."\n";
?>

Und nu?

Nun viel Spass… Ich bin auf der Suche nach der EBNF für RegExps, wenn die jemand hat 🙂 …. Ihr könnt den ganzen Code hier auch downloaden…

Nachtrag September 2008

Um es klarer zu beschreiben: PHP_LexerGenerator und PHP_ParserGenerator werden nur zum erzeugen des Lexers und des Parsers gebraucht. Es werden zwei php-Dateien generiert, die ganz normal verwendet werden können.

Moodle Block Resource-Download

Der Moodle Resource Download Block

Moodle Resource Download Block Auf vielfachen Schülerwunsch hin habe ich einen Block für unser Learning Management System Moodle codiert: Den Resource Download Block. Er erlaubt den Download aller Kursdateien und Verzeichnisse in einem ZIP-Archiv. Dieses Zip-Archiv wird wie im Beitrag Zipdateien on-the-fly erstellen mit PHP dynamisch erstellt, da (in der Theorie) jeder Lernende eine andere Kursansicht haben kann.

Einen Block erstellen ist relativ einfach, wenn man dem Block Howto folgt, aber der Teufel liegt wie immer im Detail.

Configwerte

Jeder Block kann verschiedene Konfigurationswerte erfragen und erhalten. Diese unterscheiden sich aber, ob der Block global (pinned, sticky) ist, in welchem er nur von einem Ort aus konfiguriert wird oder ob er den individuellen Kursen hinzugefügt wurde, wobei dann jede Instanz ihre eigene Konfiguration hat. Ich habe mich für den zweiten Weg entschieden.

Um die Konfigurationsvariablen mit einem Defaultwert zu versehen, muss man sich in der instance_config_print() Methode darum kümmern:

if (!isset ($this->config)) {
    // ... teacher has not yet configured the block, let's put some default values here to explain things
    $this->config->exclusionregexp= block_downloader_default_exclusionregexp();
    $this->config->compression= block_downloader_default_compression();
    $this->config->maxsize= block_downloader_default_maxsize();
}

Die Datei, die das Zip zusammenstellt ist mehr oder weniger ausserhalb von Moodle, da sie nicht „als Block“ erscheinen kann. Um da an die Konfigurationsdaten zu kommen, muss man etwas mehr Aufwand treiben. Sie befinden sich base64 codiert in den Tabellen block_instance respektive blocks_pinned für globale Blocks. courseid und instanceid werden dabei vom Link im Block übergeben.

<?php

/**
 * Create the zip on the fly and push it to the browser.
 */


require_once ('../../config.php');
require_once ($CFG->dirroot . '/blocks/downloader/lib/archive.php');
require_once ($CFG->dirroot . '/blocks/downloader/lib/downloadlib.php');
require_once ($CFG->dirroot . '/lib/filelib.php');
require_once ($CFG->dirroot . '/lib/moodlelib.php');
require_once ($CFG->dirroot . '/blocks/downloader/lib/downloadlib.php');

$courseid= required_param('courseid', PARAM_INT); // Course identification
$instanceid= required_param('instanceid', PARAM_INT); // Instance of the block

// Securitycheck
[SNIP]

// Get Config Data
$where= "pagetype = 'course-view' AND visible = 1 AND id=" . addslashes($instanceid);

// Instance?
$blockarr= get_records_select('block_instance', $where . " AND pageid=" . addslashes($courseid));

// Maybe pinned? In this case we have no courseid
if (!$blockarr) {
    $blockarr= get_records_select('block_pinned', $where);
}

if (!$blockarr) {
    error("cannot find block with: " . $where . " AND pageid=" . addslashes($courseid));
    exit ();
}

// Take the first result
$block= array_pop($blockarr);

$configdata= unserialize(base64_decode($block->configdata));

print_r($configdata);

[...]

Dateien pro Kurs abholen

Das Zusammenstellen aller Pfade für Dateien, die ein Benutzer sieht und auf die er Zugriff hat ist echt mühsam. Ich wünsche mir hier ein Bisschen den Servicegedanken: Beispielsweise würde man viel Zeit sparen, wenn der Entwickler mit dem entsprechenden Wissen Funktionen wie can_read($userid, $resourceid) implementieren würde. Zusätzlich wäre man unabhängig vom verwendeten Absicherungsschema. Ich werde das zukünftig in meinen Programmen berücksichtigen.

Reaktionen

Die Community hat – wie meistens bei Moodle – sehr nett auf die Veröffentlichung reagiert. Zwei Tage nach Version 1.0.0 habe ich schon eine Slovakische Übersetzung und diverse Hinweise auf Bugs erhalten.

Zipdateien on-the-fly erstellen mit PHP

Das Problem

On-The-Fly An unserer Schule wird Moodle als LMS (Learning Management System) verwendet. Es eignet sich sehr gut um Unterrichtsunterlagen wie Skripts, Aufträge und Präsentationen zu publizieren und den Lernenden auf eine angenehme Art und Weise zur Verfügung zu stellen. In einigen Kursen gibt es sehr viele dieser Ressourcen (Dateien und Verzeichnisse) zum Download und alleine schon das Anklicken und Speichern nimmt einen grossen Teil des zur Verfügung stehenden Zeitbudgets ein.

Einzelne Ressourcen können von der Lehrperson unsichtbar gemacht werden und nicht alle Dateien eines Kurses stehen zum Download bereit. Die Ansicht ist also sehr individuell und kann sich schnell ändern. Die Dateien werden über eine Versionsverwaltung in das System eingespiesen und können sich so ausserhalb des Systems verändern. Leider verunmöglicht dies die offensichtliche Lösung, einfach ein statisches Zip-Archiv zum Download zur Verfügung zu stellen.

Screenshot Moodle

Die Lösung

Das Zip muss also dynamisch (im Speicher) generiert werden bei einem Aufruf. Fündig wurde ich bei der Klasse archive bei phpclasses.org. Neben Zip-Archiven kann sie auch tar’s, gzip’s und bzip2’s erzeugen. Ich habe einige, kleine Erweiterungen vorgenommen, die noch beschrieben werden.

Die Handhabung ist sehr einfach:

// Archivklasse einbinden:
require_once ('lib/archive.php');

// Objekt erzeugen. Das Argument bezeichnet den Dateinamen
$zipfile= new zip_file('Meine Zipdatei.zip');

// Die Optionen
$zipfile->set_options(array (
        'basedir' => "/home/me/toZip/", // Das Basisverzeichnis. Sonst wird der ganze Pfad von / an im Zip gespeichert.
        'followlinks' => 1, // Symlinks sollen berücksichtigt werden
        'inmemory' => 1, // Die Datei nur im Speicher erstellen
        'level' => 6, // Level 1 = schnell, Level 9 = gut
        'recurse' => 1, // In Unterverzeichnisse wechseln
        // Wenn zu grosse dateien verarbeitet werden, kannes zu einem php memory error kommen
        // Man sollte nicht über das halbe memory_limit (php.ini) hinausgehen
        'maxsize' => 12*1024*1024 // Nur Dateien die <= 12 MB gross sind zippen
));

// Alle Dateien im Verzeichnis /home/me/toZip/Stuff hinzufügen
// Alle ".doc" Dateien und alle Ordner im Verzeichnis /home/me/toZip/Letters hinzufügen
$zipfile->add_files(array("Stuff", "Letters/*.doc"));

// Alle ".tmp" dateien in Stuff ausschliessen
$zipfile->exclude_files("htdocs/*.tmp");

// Alle Dateien in ".svn" und "CVS" Verzeichnissen ausschliessen (Regular Expressions)
$zipfile->exclude_regexp_files('.*/CVS|.*/CVS/.*|.*/\.svn|.*/\.svn.*');

// Archiv erstellen
$zipfile->create_archive();

// Archiv zum Download anbieten
$zipfile->download_file();

// Oder speichern (vielen Dank an PHPler!!!)
$zipfile->save_file('myzip.zip', $path = '/tmp/');

Änderungen

Ich habe folgende Dinge gegenüber der original archive.php geändert:

  • Mittels exclude_regexp_files() können Dateien auf Grund von Regular Expressions ausgeschlossen werden.
  • Die maxsize Option erlaubt das Ausschliessen von Dateien die grösser sind als die angegebene Zahl in Byte.
  • Verzeichnisse unter Linux werden als Verzeichnisse behandelt (Bug?)
  • exclude_file() prüft, ob eine Datei ausgeschlossen würde

Dank PHPler lönnen nun auch mit der Methode save_file($filename, $path = '') inmemory zips als Dateien gespeichert werden.

Meine Version kann natürlich auch frei heruntergeladen werden.

Schere, Stein, Papier: Bewertungen im Kreis

Round n RoundDie Regeln von Schere, Stein, Papier sind weitherum bekannt. Zwei Mitspieler zeigen gleichzeitig ein Symbol, danach wird ausgewertet: Schere wird von Stein geschlagen → Stein wird von Papier geschlagen → Papier wird von Schere geschlagen. Die Gewinne sind im Kreis herum (zirkulär wie der geneigte Klugscheisser sagen würde). Diese Kreisbewertung gibt es oft, auch wenn mir jetzt überhaupt kein Beispiel dazu einfällt 😀 .

Nehmen wir an, wir müssten die Auswertung programmieren und bestimmen, wer gewonnen hat. Die Symbole seien Konstanten und auf Ziffern gemappt:

define("SCHERE", 0);
define("STEIN", 1);
define("PAPIER", 2);

Insgesamt gibt es 9 Fälle (ich kann 3 Symbole zeigen, mein Mitspieler kann auf jedes mit 3 anderen Symbolen reagieren: 3 mal 3 macht 9). Der Programmierer, der nach SLOCs bezahlt wird würde also 9 ifs schreiben.

Mein Ziel ist es aber, nur 3 ifs zu benötigen für die 3 Resultate: Spieler 0 gewinnt, Spieler 1 gewinnt und unentschieden.

Die erste Reduktion ist trivial: Wenn beide dasselbe Symbol zeigen, dann ist unentschieden. Damit sind wir auf 7 ifs.

Weiter hilft eine Faustregel des alten Informatikers: Wenn etwas im Kreis herum geht, ist fast immer ein Modulo beteiligt.

Die Grundüberlegung: Wenn der Spieler 0 um 1 darunter liegt (beispielsweise STEIN=1 unter PAPIER=2) dann hat Spieler 1 gewonnen. Und nun muss das ganze halt noch im Kreis herum: PAPIER liegt 1 unter SCHERE.

Wenn nicht unentschieden ist, und auch Spieler 1 nicht gewonnen hat, ja dann, hmm, hat wohl Spieler 0 gewonnen!

Code!

define("SCHERE", 0);
define("STEIN", 1);
define("PAPIER", 2);

[...]
    if($symbolFromPlayer0==$symbolFromPlayer1) {
        // Unentschieden:
    } elseif( ($symbolFromPlayer0+1)%3 == $symbolFromPlayer1) {
        // Player1 hat gewonnen
    } else {
        // Player0 hat gewonnen
    }
[...]

Jou, juhu, hat geklappt! Wir haben unsere 3 ifs…

Observer Pattern in PHP mit der SPL

Das Observer Pattern

Observed!Normalerweise läuft es mir kalt den Rücken runter wenn ich das Wort Pattern höre. Ich will keine Fassaden, Dekoratöre und Factories in meinen Applikationen (ausser für Anwendungen in der entsprechenden Problemdomäne). An Patterns lastet der Geruch von CRC-Karten, Pair-Programming, millionenzeiliger, selbstgeschriebener Frameworks die Dinge vereinfachen sollen die man nicht begriffen hat und anderen Dingen an, die ich nicht so mag 🙂 .
Eine Ausnahme ist das Observer-Pattern (was hat es wohl angestellt, dass es auch Pattern geschimpft wird?). Grundsätzlich geht es darum, dass eine Klasse für Daten oder Ereignisse zuständig ist (das Subject). Andere Klassen können sich als Beobachter (Observer) anmelden und werden bei Statusänderungen informiert. MVC (Model-View-Controller) macht rege davon Gebrauch, indem das Model die Daten verwaltet (Subject) und Views (Observer) die Daten darstellen. Eine View könnte beispielsweise für eine Baumansicht verantwortlich sein, während sich eine andere View um eine listenartige Ansicht kümmert. Ändern sich nun die Daten, so werden beide benachrichtigt und können ihre Ausgaben anpassen. So ist es einfach, neue Klassen hinzuzufügen die auf Statusänderungen reagieren können müssen.

Das Observerpattern mit SPL

SPL ist die Standard PHP Library die ab PHP 5.0 dabei ist, stetig weiterentwickelt wird und ohne zusätzlichen Aufwand/Code verwendet werden kann. Sie erlaubt ein paar ganz nette Gags, aber das soll hier nicht Thema sein.

Ohne SplObjectStorage

Leider sind Beispiele für die PHP Implementierung etwas knapp gesät. Neben PHP Avancado gibt es nur wenig Code. Die offizielle SPL-Seite hat zwar viel Doku aber leider auch keine Beispielimplementierung.

Darum hier etwas Code. Die Klasse Ticker generiert jede Sekunde einen Tick. Einige Observer beobachten sie und reagieren darauf.

Die erste Version arbeitet ohne SplObjectStorage, damit das Prinzip klar wird. Man muss sich somit selber um die Speicherung der Observer kümmern.

<?php
/**
 * Observer pattern without SplObjectStorage
 *
 * Little example because there are not much of them on the net.
 * It implements a Model-View relationship.
 * Taken from <a href="https://blog.oncode.info">Technik, Gothic und Anderes</a>.
 * Needs at least PHP 5.1
 *  
 * @see http://www.php.net/~helly/php/ext/spl/interfaceSplObserver.html
 * @see http://phpavancado.net/comment/reply/410
 * @see http://wiki.cc/php/?title=SplObserver
 * @author Skaldrom Y. Sarg
 */


/**
 * The Class we are observing
 *
 * This class generates a tick every second.
 */

class Ticker implements SplSubject {
    protected $numTicks= 0; // Stores the ticks
    protected $observers= array (); // Stores all observers

    /**
     * Needed to register observers
     *
     * Note the typehinting here.
     */

    public function attach(SplObserver $observer) {
        $this->observers[]= $observer;
    }

    /**
     * Needed to unregister observers
     *
     * Note the typehinting here.
     */

    public function detach(SplObserver $observer) {
        // Not implemented here
    }

    /**
     * Notify all Observers that we have changed
     */

    public function notify() {
        foreach ($this->observers as $obj) {
            $obj->update($this);
        }
    }

    /**
      * Get the number of ticks
      */

    public function getNumTicks() {
        return $this->numTicks;
    }

    /**
     * The ticker itslef
     */

    public function doTicks() {
        while ($this->numTicks < 10) {
            sleep(1); // Sleep for one second
            $this->numTicks++;
            echo "\nTicker: Added a Tick (" . $this->numTicks . ")\n";
            $this->notify(); // Tell all observers we have changed         
        }
    }
}

/**
 * Observer that counts the ticks
 */

class Observer1 implements SplObserver {
    public function update(SplSubject $subject) {
        echo "Observer 1: Wow, a Tick!!! We are at tick number " . $subject->getNumTicks() . "\n";
    }
}

/**
 * Observer that calculates the number of ticks until 10
 */

class Observer2 implements SplObserver {
    public function update(SplSubject $subject) {
        echo "Observer 2: TickTick, there are " . (10 - $subject->getNumTicks()) . " ticks left until 10\n";
    }
}

$ticker= new Ticker(); // The subject to be observed

$observer1_1= new Observer1(); // First observer of type 1
$ticker->attach($observer1_1);

$observer1_2= new Observer1(); // Another observer of type 1
$ticker->attach($observer1_2);

$observer2= new Observer2(); // Observer of type 2
$ticker->attach($observer2);

// Start the run
$ticker->doTicks();
?>

Und die Ausgabe (im Sekundentakt):

Ticker: Added a Tick (1)
Observer 1: Wow, a Tick!!! We are at tick number 1
Observer 1: Wow, a Tick!!! We are at tick number 1
Observer 2: TickTick, there are 9 ticks left until 10

Ticker: Added a Tick (2)
Observer 1: Wow, a Tick!!! We are at tick number 2
Observer 1: Wow, a Tick!!! We are at tick number 2
Observer 2: TickTick, there are 8 ticks left until 10
[...]

SplObjectStorage

SplObjectStorage nimmt uns einiges ab. Er implementiert aber das Interface SplSubject nicht. SplObjectStorage hat eine Implementierung von attach() und detach() und SplSubject auch, aber eine andere 🙁 . Nun muss man halt zusammenleimen, sonst wärs noch kürzer…

<?php
/**
 * Observer pattern with SplObjectStorage
 *
 * Little example because there are not much of them on the net.
 * It implements a Model-View relationship with the help of the SPL object storage class.
 * Taken from <a href="https://blog.oncode.info">Technik, Gothic und Anderes</a>.
 * Needs at least PHP 5.1
 *  
 * @see http://www.php.net/~helly/php/ext/spl/classSplObjectStorage.html
 * @see http://wiki.cc/php/?title=SplObjectStorage
 * @author Skaldrom Y. Sarg
 */


/**
 * The Class we are observing
 *
 * This class generates a tick every second.
 */

class Ticker extends SplObjectStorage implements SplSubject {
    protected $numTicks= 0; // Stores the ticks

    /**
     * Needed to register observers
     *  
     * This method glues plObjectStorage::attach() and SplSubject::attach(). Otherwise you will get something like:
     *  "Fatal error: Declaration of SplObjectStorage::attach() must be compatible with that of SplSubject::attach()"
     */

    public function attach(SplObserver $observer) {
        // Duplicate-Checking is done in SplObjectStorage::attach()
        parent :: attach($observer);
    }

    /**
     * Needed to unregister observers
     *  
     * This method glues plObjectStorage::detach() and SplSubject::detach(). Otherwise you will get something like:
     *  "Fatal error: Declaration of SplObjectStorage::detach() must be compatible with that of SplSubject::dettach()"
     */

    public function detach(SplObserver $observer) {
        // Duplicate-Checking is done in SplObjectStorage::attach()
        parent :: detach($observer);
    }

    /**
     * Notify all our observers
     *
     * The observers are stored in the SplObjectStorage and this class implements an iterator.
     */

    public function notify() {
        foreach ($this as $observers) {
            $observers->update($this);
        }
    }

    /**
      * Get the number of ticks
      */

    public function getNumTicks() {
        return $this->numTicks;
    }

    /**
     * The ticker itslef
     */

    public function doTicks() {
        while ($this->numTicks < 10) {
            sleep(1); // Sleep for one second
            $this->numTicks++;
            echo "\nTicker: Added a Tick (" . $this->numTicks . ")\n";
            $this->notify(); // Tell all observers we have changed         
        }
    }
}

/**
 * Observer that counts the ticks
 */

class Observer1 implements SplObserver {
    public function update(SplSubject $subject) {
        echo "Observer 1: Wow, a Tick!!! We are at tick number " . $subject->getNumTicks() . "\n";
    }
}

/**
 * Observer that calculates the number of ticks until 10
 */

class Observer2 implements SplObserver {
    public function update(SplSubject $subject) {
        echo "Observer 2: TickTick, there are " . (10 - $subject->getNumTicks()) . " ticks left until 10\n";
    }
}

$ticker= new Ticker(); // The subject to be observed

$observer1_1= new Observer1(); // First observer of type 1
$ticker->attach($observer1_1);

$observer1_2= new Observer1(); // Another observer of type 1
$ticker->attach($observer1_2);

$observer2= new Observer2(); // Observer of type 2
$ticker->attach($observer2);

// Start the run
$ticker->doTicks();
?>

Fazit

Man beachte: Hier könnten noch viele beliebige Observer hinzugefügt werden, ohne dass das Subject (Ticker) verändert werden muss.