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.