Objektorientierte Entwicklung vs. PHP 4

05 05 2010

Zur Zeit arbeite ich an einem Wordpress-Projekt und bin nach der Umstellung auf PHP 5.3 (vorher war versehentlich PHP 4 auf dem Webspace aktiv) über etliche Deprecated-Warnungen gestolpert. Die explizite Zuweisung von Objekten per Referenz (also mit =& statt dass das Objekt mit = geklont wird) in der wp-settings.php ist schuld, denn dieses im Grunde erwartungskonforme Verhalten ist seit PHP 5 der Normalfall, Objekte werden jetzt nur noch geklont, wenn man explizit clone benutzt, so dass die explizite Zuweisung per Referenz überflüssig ist und angemahnt wird.

Nun ist es bei einem guten Hoster ein Handgriff ein, die E_DEPRECATED-Warnungen in der php.ini zu unterdrücken, von daher ist das alles halb so wild. Die Sache ist aber ein Symptom eines großen Dilemmas: Will man – aus welchen Gründen auch immer – kompatibel zu PHP 4 bleiben, muss man manchmal Code schreiben, der in PHP ab 5.3 eine Deprecated-Warnung wirft. Das TYPO3-Backend beispielsweise warf bis vor kurzem (vielleicht auch immer noch) ebenfalls Unmengen an Deprecated-Warnungen, weil es intensiven Gebrauch von eregi-Funktionen macht. Die kann man mit gutem Geschwindigkeitsgewinn und ohne Schwierigkeiten mit PHP 4 gegen preg-Funktionen austauschen, hier ist es also lediglich eine Sache von Fleiß; ganz davon abgesehen, dass TYPO3 sowieso seit Jahren kein PHP 4 mehr unterstützt. Die Sache bei Wordpress und einigen anderen Projekten mit Objektorientierten Elementen ist aber eine Zwickmühle.

Wobei es in meinen Augen keineswegs eine Zwickmühle ist, da die Lösung auf der Hand liegt: Man wirft einfach den PHP 4 Support über Bord und hält die Anfeindungen einiger Ewiggestriger aus, die nicht wissen, wie sie bei ihren Webspace eine aktuelle PHP-Version umstellt. Diese Anfeindungen kommen leider vor, und sind einer der Gründe, wieso ich zu Serendipity keinen Code mehr beitrage. Serendipity bewahrt wie Wordpress noch immer die PHP 4 Kompatibilität und steht einer zukunftsgerichteten Weiterentwicklung damit massiv im Weg. Wordpress wird wohl ab Version 3.0 (also ab demnächst) PHP 5 vorraussetzen, damit es endlich nach vorne gehen kann. Das ist im Falle von Wordpress auch bitter nötig, wenn man sich den mitunter grauenerregenden Code anguckt. Die aktuelle Beta 1 wirft aber leider immer noch die Deprecated-Warnungen.

Es gibt so etwas wie Codehygiene. Ich frage mich, wieso sich so viele Softwareprojekte mit aller Macht dagegen stemmen, ihren Code ein wenig zu warten. Eregi-Funktionen gelten schon seit mindestens fünf Jahren als um Größenordnungen langsamer, als ihre preg-Pendants. Wieso zur Hölle liest man dann als Abhilfe für Deprecated-Meldungen immer und überall, dass man die Warnungen ausschalten soll? Wo ist das Problem, sich einmal eine Nacht hinzusetzen und die veralteten und langsamen eregi-Funktionen zu eliminieren? Wieso wird unter Verzicht auf fundamental wichtige Programmiertechniken der Objektorientierung auch mehrere Jahre nach Einstellung des Supports für PHP 4 so sehr daran geklebt? PHP 4 ist veraltet, langsam und behindert massivst eine saubere Programmierung. Das ist sogar so offensichtlich, dass es jedem auffallen müsste, der nur ansatzweise objektorientiert mit PHP programmiert. PHP 4 fehlt es an fundamentalen Funktionalitäten an allen Ecken und Enden, nicht nur bei der Objektorientierung. Bitte, hört auf mit der falsch verstandenen Rückwärtskompatibilität und blickt mal nach vorne.

P.S. Ich bin übrigens latent auf der Suche nach einem neuen Blogsystem. Serendipity möchte ich den Rücken kehren, weil sich an der Front scheinbar nichts mehr tun wird. Auf PHP 5 umstellen? Neue Programmierkonzepte zulassen? Nix da, alles bleibt, wie es ist. Ein System, dessen Core so ungerne angefasst wird, ist nicht meins. Davon abgesehen, dass der Core in meinen Augen sowieso gar kein HTML ausgeben sollte. S9Y ist super stabil und funktioniert bei mir seit nunmehr fast sechs Jahren ohne jedes nennenswerte Problem vor sich hin, aber in etwa so lange hat sich auch nicht mehr wirklich etwas nennenswertes weiter entwickelt; das stimmt zwar nicht ganz, aber die Änderungen blieben insgesamt doch sehr dezent. Wordpress ist leider keine Alternative, auch wenn das Backend großartig ist. Ein Blick in den Core an beliebiger Stelle sollte ausreichend Anlass geben, das System nicht zu wollen. Was ist eigentlich aus Habari geworden?


Wordpress hat schlimmen Code, andere aber auch

07 09 2009

Wer ernsthaft beruflich oder auf hohem Niveau in seiner Freizeit mit PHP arbeitet und älter als 15 ist, dem stehen wahrscheinlich beim Blick in den Quellcode von Wordpress die Haare zu Berge. Code is art ist ein wunderschöner Wordpress-Slogan, der in dem Kontext aber wirklich mehr als unangebracht ist. Schlimmer als der zusammengezimmerte Kern von Wordpress, der aktuell mal wieder mit einem notdürftigen Flicken gegen den grassierenden Wordpress-Wurm repariert wurde, sind aber die meisten PlugIns. Viele sind offenbar von blutigsten PHP-Anfängern schnell und ergebnisorientiert runterprogrammiert worden, was von Wordpress ja geradezu provoziert wird und was auch großen Anteil an der Beliebtheit des Systems trägt. Jeder ahnungslose Anwender kann mit ein paar Zeilen PHP-Code schnell das erreichen, was er gerade braucht. Das erinnert mich an meine Anfänge mit PHP im Jahr 2001 mit dem damals herausragenden phpBB 1.4. Hier war im Grunde alles hartkodiert und wenn einem irgendwas nicht gefiel, hackte man irgendwie im Quellcode herum. Ein Templatesystem wurde erst mit Version 2 eingeführt und auch hier hackte man noch alle möglichen MODs in das System, was ein Update praktisch unmöglich machte. Folge waren haarsträubende Sicherheitslücken, die nicht gestopft wurden. Wordpress vermeidet dieses Modding immerhin mit einem sehr flexiblen PlugIn-System, der Zugang für Dilettanten und Anfänger wurde dadurch aber noch einfacher. Das Ergebnis sehen wir zur Zeit, Gerrit van Aaken hat das schon zusammengefasst: Bei Wordpress hilft nur ein schmerzhafter Neuanfang, so wie TYPO3 das momentan auch vollzieht und wie phpBB das auch schon zwei mal hinter sich hat.

Nun will ich nicht so viel auf Wordpress rumhacken, wenn ich es selber gar nicht benutze. Im Grunde ist das System großartig, wenn man die miese Codebasis ignoriert. Ich will auf etwas ganz anderes hinaus, nämlich die Codequalität von Serendipity-PlugIns. Wann immer ich ein Seitenleisten-PlugIn installiert habe, musste ich dessen Code bearbeiten, weil die HTML-Ausgabe standardmäßig schlimm aussah oder gar fehlerhaft war. Das PlugIn-System von S9Y ist auf den ersten Blick recht kompliziert, was totale Dilettanten vom PlugIn schreiben abhält; das ist schon mal gut. Trotzdem ist die Codequalität der PlugIns oft eher mäßig. Fast überall wird zum Beispiel der HTML-Code mit echo direkt da ausgegeben, wo er anfällt, natürlich mit irgendwelchem HTML-Code drumherum, den der Autor gerade für angemessen hielt. Kein Wunder also, dass der Output nicht immer angemessen ausfällt. Ich habe vor einiger Zeit ein eigenes Seitenleisten-PlugIn für Twitter geschrieben, weil das alte u.a. genau dieses Problem zeigte. Ein Kernfeature meines PlugIns war der Einsatz einer Templating-Engine. Die zu implementieren war PHP-seitig ein Kinderspiel für jeden halbwegs erfahrenen PHP-Programmierer. Die Frage ist nun, warum zur Hölle von den paar offiziell verfügbaren PlugIns noch fast keines so ein Templating-System mitbringt? Es täte S9Y wirklich immens gut, wenn sich mal jemand hinsetzen würde und ein paar der PlugIns auf den aktuellen Stand bringen würde. Also zumindest eine Templating-Engine einbauen und die Ausgabe entsprechend anpassen, aber auch manch andere Routine könnte mal überarbeitet werden. Gegenüber Wordpress ist das Jammern auf hohem Niveau, aber auch S9Y ist merklich in die Jahre gekommen, vor allem seine PlugIns.

Wenn ich sowas lese, pflege ich zu antworten, derjenige soll nicht jammern, sondern selber anpacken. Das gilt auch für mich, sicher. Allerdings habe ich immens schlechte Erfahrungen mit meinem Twitter-PlugIn gemacht. Das einzige konstruktive Feedback kam vom unglaublich engagierten Chefprogrammierer selber, der mir meinen ursprünglich geplanten Arbeitsaufwand mit Änderungswünschen um das Mehrfache aufgeblasen hat. Am Ende war das neue PlugIn wirklich großartig geworden, ich bin richtig stolz darauf. Die ganzen angeforderten Änderungen kreisten alle um funktionale Anpassungen an das alte PlugIn, so dass mein neues PlugIn alle Funktionen des alten hatte, nur eben diesmal in gut. Dann kam das Problem: Es gab kein Feedback von Dritten und stattdessen hat mir ein ätzender Troll auch noch ans Bein gepisst und einen extra Thread im Forum aufgemacht, um mich als Schnösel zu dissen (weil ich PHP4 Nutzer, die PHP4 Kompatibilität einfordern, als Ewiggestrige bezeichnet habe und er sich davon angesprochen fühlte). Ich habe mein Engagement für S9Y daraufhin nach ein paar Tagen Diskussion eingestellt, denn für kostenlose und gute Programmierarbeit möchte ich echt alles andere haben als von irgendwelchen undankbaren Typen beschimpft zu werden. Mein PlugIn ist übrigens nie im Repository erschienen und stattdessen gibt es ein funktional wirklich krasses anderes PlugIn; hoffentlich ist wenigstens meine Arbeit da eingeflossen und war nicht völlig umsonst. Wie auch immer: Ich werde keine Arbeit mehr zu S9Y beitragen; wenn ich etwas neu baue, reiche ich das im Forum ein und wenn sich niemand drum kümmert, soll mir das egal sein.


Nur halb so schlimm: Eigene Short-URLs

05 09 2009

Vor einiger Zeit habe ich gefordert, dass größere und oft bei Twitter verlinkte Seiten eigene kurze URLs für ihren Content anbieten. Neulich erst habe ich mir dann Gedanken gemacht, wie man Short-URLs uns so verüberflüssigen kann. Auf diese große Lösung mag ich nicht warten, aber zumindest bei der kleinen Lösung kann ich mit gutem Beispiel voran gehen. Also habe ich gerade schnell ein kurzes Script geschrieben, das kurze URLs für mein Blog in die vollen URLs umsetzt. Zum Beispiel lautet für diesen Eintrag die kurze URL http://spackblog.de/668, was mit 23 Zeichen deutlich kürzer ist als die verdammt lange volle URL und vor allem kürzer als 30 Zeichen, ab denen Twitter ungefragt mit bit.ly verkürzt.

Ich hatte eine Weile überlegt, diese Funktionalität als Serendipity-PlugIn zu veröffentlichen, aber das habe ich aus verschiedenen Gründen nicht gemacht. Ein Grund ist, dass mich ein undankbarer Vollspacko im S9Y-Forum angesaugt und als Schnösel bezeichnet hat, als ich mein letztes PlugIn dort vorgestellt habe. Ich will da gar nicht genauer drauf eingehen, Folge ist jedenfalls, dass meine Lust auf die extra Arbeit für ein S9Y-PlugIn dadurch doch sehr gesunken ist. Ein weiterer Grund ist, dass der generierte Link irgendwo im Template und noch mal im Header positioniert werden muss, damit es Sinn macht. Das kann ein PlugIn nicht flexibel alleine regeln. Man könnte den Link allenfalls dort hinpacken, wo momentan auch die Tags sind, aber das gefällt sicher nicht jedem. An die .htaccess des Blogs, oder wenn dieses in einem Unterverzeichnis steckt auch an die .htaccess der Domain muss man auch noch ran. Spricht alles gegen ein PlugIn. Sowas gehört in meinen Augen sowieso in den S9Y-Core.

Das Script funktioniert super simpel. Alles fängt mit einer Anweisung in der .htaccess an, die alles, was nach kurz-URL aussieht an das Script weiterreicht:

RewriteEngine On
RewriteBase /
RewriteRule ^(e|a|c)?([0-9]+)/?$ s9y_shorturl.php?type=$1&id=$2 [NC,L]

Man kann schon sehen, dass das Script auch kurze URLs für Kategorien und Autoren unterstützt. Der spannende Teil im Script selber ist die Datenbankabfrage, die die kurze URL aus der Datenbank ausliest:


// open a database-connection
$dbh = new PDO('mysql:host=' . $db_server . ';dbname=' . $db_database, $db_user, $db_pass);

// prepare the statement
$stmt = $dbh->prepare('SELECT permalink
  FROM s9y_permalinks
  WHERE type = :type
    AND entry_id = :entry_id
  LIMIT 1');

// execute the statement
$stmt->execute(array(':type' => $type, ':entry_id' => $entry_id));
  
// read the returned urlpart
$urlpart = $stmt->fetchColumn();

// close the database-connection
$dbh = null;

// redirect the user with a 301 status code (moved permanently)
header('Location: ' $blog_base_url . $urlpart, true, 301);
exit;

Der Rest vom Script besteht aus Fehlerbehandlung und Variablen-Vorbereitung, die ich hier mal weg lasse. Wer den ganzen Code haben will, kann sich gerne bei mir melden. Damit ist der erste Teil schon fertig, der die kurzen URLs auflöst. Für die kurzen URLs werden einfach die von Serendipity vergebenen IDs genutzt, der Parameter type kann übrigens 'entry', 'category' oder 'author' sein. S9Y führt eine eigene Tabelle für die Zuordnung von IDs und URLs, die man nur abfragen muss.

Der zweite Teil ist leider Handarbeit im Template. Man muss dazu an der Stelle, wo der Link auftauchen soll folgenden Code in der entries.tpl platzieren: <a href="http://SHORTURL_DOMAIN/{$entry.id}">http://SHORTURL_DOMAIN/{$entry.id}</a>. SHORTURL_DOMAIN muss natürlich durch die jeweilige URL ersetzt werden, in meinem Fall ist das tatsächlich eine andere Domain als die, auf der das Blog läuft. Ich habe zusätzlich noch in der index.tpl im Headerbereich im {if $entry.id} Block einen Short-URl Autodiscovery Link eingefügt, wie er hier beschrieben ist.

Das ganze arbeitet extrem simpel und macht genau das, was es soll: Wenn jemand meinen Blog bei Twitter und Co. verlinken will, kann jetzt meine 23 stellige Kurz-URL hernehmen und sich so den Scheiß mit den verkürzten URLs sparen, bei denen man nicht weiß, wohin sie eigentlich linken. Alle Seiten sollten solche eigenen Kurz-URLs haben, dann wäre das mit Twitter nur noch halb so schlimm.


Statische Methode ruft Constructor auf

14 04 2009

Ich hocke gerade an einem mod_rewrite Problem mit dem vielversprechenden CMS Concrete5. Jetzt muss mir mal jemand erklären, wieso etwas so programmiert sein könnte, wie es da gemacht wurde:

Ganz am Anfang des Ablaufs (im Dispatcher) wird indirekt die Klasse "Request" initialisiert: $req = Request::get();. Diese statische Methode get() holt sich die Request-Parameter (da liegt übrigens mein Problem) und instantiiert dann mit $req = new Request($path); eine neue Instanz der eigenen Klasse und gibt diese zurück. Jetzt frage ich mich, warum zur Hölle instatiiert nicht der Dispatcher die Klasse und deren Constructor ruft die get() Methode auf (oder macht die paar Zeilen direkt im Constructor)? Warum wird von einer Klasse eine statische Methode aufgerufen, die eine neue Instanz der eigenen Klasse aufmacht und zurückgibt? Wozu also der Umweg über die statische Methode? Ist sowas irgendwo üblich? Wenn ja, was wird bezweckt?

Mein Respekt gilt schon mal allen, die bis hierher gelesen haben. ;)


Mein Lifestream ist startklar

26 02 2009

Seit gestern ist meine Lifestream-Seite online und seit gerade eben auch der dazugehörige RSS-Feed. Damit biete ich Euch, liebe Leser, eine halbwegs übersichtliche und optisch erträgliche Möglichkeit, auf einen Blick zu sehen, was es bei mir Neues gibt.

Momentan könnt ihr dort neben Anreißern zu den jeweils letzten fünf Blogeinträgen und Kommentaren auch meine zehn neuesten Microblog-Einträge (Dents) sofort sehen. Wer etwas scrollt, findet weiter unten zehn neue Wong-Bookmarks und fünf neue Qype Bewertungen. Wenn ihr mit der Maus einen Moment über den Links verweilt, könnt ihr im Tooltip jeweils die Beschreibung bzw. einen Anreißer für den Text lesen.

Rechts daneben gibt es Links für alle Feeds einzeln und für den Gesamt-Feed. Der ist für diejenigen unter Euch, die Feeds nutzen, vielleicht eine gute Sache, bietet er doch alle meine aktuellen Inhalte chronologisch sortiert und halbwegs sinnvoll aufgearbeitet für den Feedreader der Wahl an. Ich habe mich hier entschieden, Titel und Autor möglichst aussagekräftig zu gestalten. Ich hoffe das ist mir gelungen.

Ich bin weiterhin und stets offen für Feedback.

Zum technischen Kram: Das Script für den ganzen Lifestream habe ich selber in PHP geschrieben und greife dabei auf das feine SimplePie für das Feed-Handling zurück, das ich um einige Funktionen zur Angleichung der Feedinhalte bereichert habe. Dazu musste ich die SimplePie-Klasse etwas erweitern und ein wenig was drumrum bauen. Wenn Interesse besteht, kann ich das Skript gerne unter einer liberalen Lizenz zur Verfügung stellen. Es ist recht allgemein gehalten und zentral konfigurierbar, vom Template für die Lifestream-Seite natürlich mal abgesehen, das muss jeder für sich selbst und jeden einzelnen Feed anpassen. Alternativ kann man auch ohne viel Eigenarbeit nur den Feedaggregator mit den feinen Angleichungsfunktionen nutzen: Feeds einfach nur zusammen mischen funktioniert nämlich nicht wirklich gut.


Eine kleine Fuck IE6 PHP Klasse

24 02 2009

Aktuell soll ja wieder mal der IE6 endlich sterben. In Norwegen sind einige große Nachrichtenseiten mit einer IE6-Warnung unterwegs und der IE6 Death March ist auch unterwegs. Ich verkaufe schon seit einiger Zeit IE6-Anpassungen nur noch als extra zu bezahlende Zusatzoption, wobei es bei mir dank semantisch sinnvollem Code hier nur um optische Feinheiten geht, die ich für entbehrlich halte. Wie auch immer: Bisher hat kein Kunde Aufpreis für den IE6 zahlen wollen. Geht doch.

Nun baue ich gerade meine Lifestream-Seite auf und befreie mich bei der Gelegenheit von jedem IE6-Mist. Meine Contentboxen hat der IE6 schlicht gar nicht angezeigt (wohl aber konnte man die unsichtbaren Links anklicken), so dass ich mich entschieden habe, dem IE6 und älteren Versionen einfach alle Styles vollständig wegzunehmen und stattdessen eine rote Alarm-Box einzublenden. Die Inhalte bleiben vollständig nutzbar, es sieht nur total scheiße aus. Sehr befriedigend, kann ich so den IE6-Nutzern doch etwas von der Hässlichkeit vermitteln, die dem IE6 innewohnt.

Zu diesem Zweck habe ich eine kleine statische PHP Klasse geschrieben, die ich hiermit gerne zur Verfügung stellen möchte. Wer Lust hat, kann die einfach für sich anpassen und in seine Projekte einbinden. Ich habe mich gegen eine clientseitige Lösung mit CSS und/oder JavaScript entschieden, weil ich "gute" Besucher nicht mit dem für sie sowieso unsichtbaren IE6-Code belasten wollte. Ich denke, das ist eine gute Idee.

Die Klasse ist recht simpel gestaltet und schreit geradezu danach, bei Bedarf verfeinert zu werden (mir reicht das erst mal so). Also in Kürze:

  1. Einbinden der Klasse, etwa indem man den Code direkt in sein Projekt kopiert oder als Datei abspeichert und mit require_once('PFAD/fuck_ie6.class.php'); einbindet.
  2. Da die Klasse statisch ist, muss sie nicht instanziiert werden. Man ruft also die drei statischen Methoden wo man sie braucht.
  3. Folgende Methoden und Variablen stehen zur Verfügung:
    • fuck_ie6::is_ie6() gibt true zurück, wenn der Besucher mit einem IE4, 5 oder 6 unterwegs ist, sonst false. Praktisch, wenn man bestimmte Inhalte wie Stylesheets ohne Conditional Comments für diese Besucher aus- oder einblenden möchte. Im Prinzip ist das nur eine sehr simple RegEx.
    • fuck_ie6::print_style() und fuck_ie6::print_alert_box() printen den Inhalt der Variablen $alert_style und $alert_content in Abhängigkeit vom Browser des Besuchers. Die beiden Methoden ersparen einem also nur die if-Abfrage mit fuck_ie6::is_ie6() im Template ein.
    • Die beiden Variablen $alert_style und $alert_content enthalten den Style und den Code, die für die Alarm-Box genutzt werden sollen. Hier finden Anpassungen an die eigenen Wünsche statt.

<?php
/**
* a very simple static class for sniffing Internet Explorer 6 and below
*
* can detect whether the user comes with an annoying IE (version 4,5 or 6)
* additionally holds content and styles for a red alert box
*
* @author	Gregor Nathanael Meyer <Gregor [at] der-meyer.de>
* @license  http://creativecommons.org/licenses/by-sa/3.0/de/ Creative Commons cc-by-sa
* @version  0.1  first release
*/
class fuck_ie6
{
  /**
    * the style block used for the alert box
    * @static
    */
  public static $alert_style = '  <style>
  .errorBox {
    background: #fbe3e4;
    color: #8a1f11;
    border: 2px solid #fbc2c4;
    width: 80%;
    padding: 25px;
    margin: 0 auto;
    font-size: 1em;
    line-height: 1.3em;
  }
  </style>';
  
  /**
    * the HTML of the alert box
    * @static
    */
  public static $alert_content = '<p class="errorBox"><strong>Alarm:</strong> Offensichtlich bist Du mit einem alten Internet Explorer (6.0 oder älter) unterwegs. Da dieser Browser aus dem Jahr 2001 die auf dieser Seite genutzten modernen Webstandards nicht hinreichend unterstützt, habe ich das visuelle Beiwerk für Dich deaktiviert. Die Inhalte sind weiterhin erreichbar.</p>';
  
  
  /**
    * the IE6 detector function
    * uses just a simple RegEx to read the UA string
    * @static
    */
  public static function is_ie6()
  {
    return preg_match('#^Mozilla/4.0 \(compatible; MSIE [456]#i', $_SERVER['HTTP_USER_AGENT']) ? true : false;
  }
  
  /**
    * prints the style block in case of IE<=6
    * @static
    */
  public static function print_style()
  {
    if ( self::is_ie6() )
    {
      echo self::$alert_style;
    }
  }
  
  /**
    * prints the alert box content in case of IE<=6
    * @static
    */
  public static function print_alert_box()
  {
    if ( self::is_ie6() )
    {
      echo self::$alert_content;
    }
  }
}

Ein Beispiel könnt ihr bei meinem Lifestream sehen.


Konzentriertes Arbeiten

21 11 2008

Ich sitze seit drei Stunden am Rechner und baue "mal eben" ein "kleines" Script für ein Kontaktformular. Wie immer halt. Gerade will ich ins Bett gehen und da fällt mir auf, dass ich die ganze Zeit einen Kopfhörer trage, aber keine Musik an habe. Zwischendurch habe ich mir immer wieder den Kopfhörer zurecht gerückt, aber mir ist die ganze Zeit nicht aufgefallen, dass ich vergessen habe, Musik an zu machen. Sowas nenne ich mal konzentriertes Arbeiten.

So, ab ins Bett. Die letzte Stunde habe ich übrigens damit verbracht, eine SMTP-Versand-Klasse für PHP (PHPMailer) zu integrieren und herauszufinden, dass ich das an sich korrekt gemacht habe, der FH-Mailserver von unserem Fachbereichs-Webserver aber keine Mails per SMTP (mit Auth) annehmen mag. Mist.


array_merge_recursive_overwrite()

19 06 2008

Update 30.70.2009: Seit PHP 5.3.0 gibt es endlich die Funktion array_replace_recursive(), die genau das kann, was meine array_merge_recursive_overwrite() macht. Ich habe meine Funktion erweitert, so dass sie lediglich array_replace_recursive() aufruft, wenn diese verfügbar ist. Dies dient nur der Abwärtskompatibilität in bestehenden Projekten und macht keinen Sinn, wenn man neu anfängt. Für einen solchen Fall (hoffentlich der Normalfall) habe ich unter obigem Link einen fertigen Code als Kommentar hinterlassen.

Gestern musste ich mit PHP zwei verschachtelte assoziative Arrays zusammenführen, wobei allerdings bestehende Werte im einen Array durch Werte des anderen überschrieben werden sollten. Leider kommt die PHP-Funktion array_merge_recursive() dafür nicht in Frage, weil sie im Konfliktfall die Werte nicht überschreibt, sondern ein Unterarray baut, in dem beide Werte enthalten sind. Einen Schalter zum Überschreiben gibt es nicht, also musste ich eine eigene Funktion für diesen Zweck bauen. Et voilà, hier ist sie:

/**
 * merges two arrays recursively, overwrite existing values in $base with values from $merge
 * array_merge_recursive does not overwrite values, it creates a new sub array with both values in it, so we need this function
 * since PHP 5.3.0 the built in array_replace_recursive() does the same, so it will be called, if available
 * 
 * @author     Gregor Nathanael Meyer <Gregor at der-meyer.de>
 * @param      array $base base array
 * @param      array $merge array to be merged into base array
 * @param      array $merge,... more merge arrays
 * @return     array merged inputs
 */
function array_merge_recursive_overwrite($base, $merge)
{
  // as of PHP 5.3.0 array_replace_recursive() does the work for us
  if (function_exists('array_replace_recursive'))
  {
    return call_user_func_array('array_replace_recursive', func_get_args());
  }
  
  function recurse($base, $merge)
  {
    foreach ($merge as $key => $value)
    {
      // create new key in $base, if it is empty or not an array
      if (!isset($base[$key]) || (isset($base[$key]) && !is_array($base[$key])))
      {
        $base[$key] = array();
      }
      
      // overwrite the value in the base array
      if (is_array($value))
      {
        $value = recurse($base[$key], $value);
      }
      $base[$key] = $value;
    }
    return $base;
  }
  
  // handle the arguments, merge one by one
  $args = func_get_args();
  $base = $args[0];
  if (!is_array($base))
  {
    return $base;
  }
  for ($i = 1; $i < count($args); $i++)
  {
    if (is_array($args[$i]))
    {
      $base = recurse($base, $args[$i]);
    }
  }
  return $base;
}

Genau wie ihre Schwester array_merge_recursive() nimmt sie zwei oder mehr Arrays entgegen und gibt diese verbunden zurück. Viel Spaß damit, falls es wer gebrauchen kann.

Nachtrag: Ich hätte auch einfach mal bei Google suchen können, so ziemlich die gleiche Funktion gibt es auch im Horde Framework, allerdings nur für zwei Arrays (also so wie meine innere Funktion). Mist, da hätte ich mir die verhasste Rekursion gar nicht selber ausdenken müssen.

Nachtrag: Die Kommentare in der PHP-Doku bringen auch etliche Varianten dieses Themas. Man sollte sich von unleserlicher Schreibweise nicht abschrecken lassen, auch wenn man schon müde ist. Naja, wenigstens ist meine Lösung nicht total doof.


Virtueller lokaler Entwicklungsserver

03 03 2008

Für die Entwicklung von Webkram habe ich einen lokalen Ubuntu-Server in einer virtuellen Maschine installiert. Dort läuft auch ein Samba-Server, über den ich auf meine Dateien zugreife. Sehr praktisch das ganze. Mache ich das Ding aus, stört mich nichts, mache ich es an, habe ich eine komplette und realistische Entwicklungs-Umgebung. Soweit nichts besonderes, aber ich wollte auf die Schnelle ein paar kleine Tipps dazu loswerden.

Für den Start der Virtuellen Maschine und die Einbindung der Netzlaufwerke benutze ich eine simple Batch-Datei:

"PFAD\ZU\VBoxManage.exe" startvm "NAMEDERVIRTUELLENMASCHINE"
pause
net use LAUFWERKSBUCHSTABE: \\SERVERNAME\FREIGABE PASSWORT /user:USERNAME /persistent:no
Die Pause ist notwendig, damit das Verbinden zu den Freigaben nicht fehlschlägt, während der Server noch startet. Folgender Code beendet die Verbindungen und hält den Server an:
net use /DELETE LAUFWERKSBUCHSTABE:
"PFAD\ZU\VBoxManage.exe" controlvm "NAMEDERVIRTUELLENMASCHINE" savestate

Durch das Einfrieren gibt es leider ein lästiges Problem: Die Uhr des Servers wird mit eingefroren, so dass nach jedem aufwachen ein sudo ntpdate ntp1.ptb.de nötig ist, um die Uhr neu zu stellen. Ansonsten ist diese Lösung wirklich gut und VirtualBox eine brauchbare und kostenlose Software. Diese Anleitung hat mir beim Einrichten des Ubuntu-Servers übrigens gute Dienste geleistet. Ach ja: Als ich das seinerzeit eingerichtet habe, musste man noch den Server-Kernel via Aptitude von einer Rettungs-CD gegen einen Standardkernel austauschen, weil der Server-Kernel von Ubuntu 7.04 in VirtualBox nicht startet. Vielleicht ist das aber auch inzwischen behoben.

Noch ein weiterer kleiner Trick zur Arbeitserleichterung ist eine Verknüpfung zum Editor der Wahl mit der Hosts-Datei, die man ab und an bearbeiten muss. In Windows ist diese Datei so dämlich tief versteckt, dass sich das schnell lohnt.


Zufällige Datensätze aus einer MySQL-Datenbank, aber schnell

13 11 2007

Letztes Wochenende habe ich mich einige Stunden mit der Optimierung einer SQL-Abfrage herumgeschlagen, die eine beliebige Zahl zufälliger Datensätze aus einer Tabelle fischen soll und dabei mit JOINs aus zwei weiteren Tabellen die zugehörigen Daten ziehen. Mein erster Ansatz war dabei folgender:

SELECT an.frage_id AS frage_id,
	an.antwort_text AS antwort_text,
	an.timestamp AS timestamp,
	fr.frage_text AS frage_text,
	fr.timestamp AS fr_timestamp,
	me.member_id AS member_id,
	me.member_name AS member_name
FROM member_antworten an
	JOIN member_fragen fr
		ON fr.frage_id = an.frage_id
	JOIN members me
		ON an.member_id = me.member_id
WHERE an.antwort_text != '#'
ORDER BY RAND() 
LIMIT 10

An sich eine zweckmäßige Query, die auch im Netz in den meisten Fällen so empfohlen wird. Nur: 5 Sekunden auf meinem lokalen Testsystem ist deutlich zu lang, zumal die gleiche Query ohne das "ORDER BY RAND" in 0,003 Sekunden durch ist. Also habe ich mich auf die Suche begeben.

Mein erster Ansatz bestand darin, die zufällige Bestimmung der Datensätze aus der Tabelle members_antworten über eine Subquery in der WHERE-Klausel zu erledigen, also lauteten die letzten drei Zeilen folgendermaßen:

/* anderer Kram… */
WHERE an.antwort_id IN (
	SELECT antwort_id
	FROM member_antworten
	WHERE an.antwort_text != '#'
	ORDER BY RAND()
	LIMIT 10
)

Wer den Abschnitt zu RAND() im MySQL-Handbuch aufmerksam gelesen hat wird wissen was jetzt passiert: Nichts. Note that RAND() in a WHERE clause is re-evaluated every time the WHERE is executed. Und das dauert bei 13000 Datensätzen eine Weile. Nach zehn Minuten hab ich also den mysql-Prozess neu gestartet. Mist.

ORDER BY RAND() bringt uns also nicht weiter, die Frage ist aber: Wie zur Hölle bekomme ich zufällige Datensätze in endlicher Zeit aus einer Datenbank. Einen Zufallswert in PHP erzeugen und in der Query übergeben kommt nicht in Frage, weil die Werte des Primärschlüssels durchaus Löcher haben können und die kennt diese Zufallszahl nicht. Dieser Artikel klang vielversprechend, dieser Folgeartikel sogar noch mehr. Kern dieser Ansätze ist eine zusätzliche Spalte mit Zufallswerten, die mit einem einmal pro Query erzeugten Zufallswert multipliziert werden und dann der Abstand dazu entscheidet, welche Datensätze selektiert werden. Interessanter Ansatz, nur mit 1,7 Sekunden noch immer recht langsam und irgendwie auch unsinnig kompliziert. Das müsste doch einfacher gehen, ich habe also weiter probiert und viel verworfen. Die Performance war immer gleich schlecht mit verschiedenen mehr oder weniger kreativen Ansätzen.

Schlussendlich bin ich bei dieser halbwegs eleganten Query gelandet, die mit 1,7 Sekunden auf etwa gleichem Niveau war:

SELECT an.frage_id AS frage_id,
	an.antwort_text AS antwort_text,
	an.timestamp AS timestamp,
	fr.frage_text AS frage_text,
	fr.timestamp AS fr_timestamp,
	me.member_id AS member_id,
	me.member_name AS member_name
FROM (
		SELECT member_id,
			frage_id,
			antwort_text,
			timestamp
		FROM member_antworten
		WHERE antwort_text != '#'
		ORDER BY RAND()
		LIMIT 10
	) an
	JOIN members me
		ON me.member_id = an.member_id
	JOIN member_fragen fr
		ON fr.frage_id = an.frage_id

Je ein Index für Spalte member_id und frage_id haben das Ding schließlich auf 0,2 Sekunden gehoben, was passabel ist. Schön an dieser Lösung finde ich, dass nur noch die nötigen Datensätze gejoint werden und man nicht die Übersicht verliert.

Falls jemand das hier gelesen hat und mir einen zweckmäßigen Tipp geben kann, wie ich das offenbar langsame ORDER BY RAND() auch aus der Subquery wegoptimieren kann, soll er sich bitte hier melden. Bei der Gelegenheit könnte mir auch mal jemand erklären, warum es keine allgemeingültige und schnelle Möglichkeit gibt, zufällige Datensätze in SQL zu selektieren. Die ORDER BY RAND()-Kiste ist zum einen sehr langsam und zum anderen ein MySQL-Dialekt (genau wie das unglaublich praktische LIMIT).

Ein interessanter Ansatz bei hoher Last wäre ein Caching des Zufalls, also dass es eine Spalte mit Zufallswerten gibt, die nur auf Anfrage oder mit einem Timer neu geschrieben werden und sonst nur gelesen. Das wäre wirklich fix, aber wenn man schon Caches einsetzt, kann man auch gleich das Ergebnis der Query cachen und braucht nicht mal mit der Datenbank zu reden. Symfony bietet übrigens für solche Zwecke ein recht flexibles Cachesystem.

P.S.: Ach ja, die zugehörige Applikation ist ein Interviewsystem in einer internen kleinen Community und das DBMS ist MySQL5, falls das jemanden interessiert.