toscho.design

PHP: Warum Output-Buffering meistens eine schlechte Idee ist

Auf den ersten Blick ist Output-Buffering sehr praktisch: Man kann ganz unbesorgt Header senden oder Output erzeugen, ohne daß irgend etwas sichtbar kaputtgeht.

<?php
error_reporting( E_ALL | E_STRICT );
header( 'Content-Type: text/html;charset=utf-8' );
ob_start();
print '<p>Das ist kein HTML.</p>';
header( 'Content-Type: text/plain;charset=windows-1252' );
print ob_get_clean();

Das … funktioniert. Der Haken an solch simplen Beispielen ist, daß man Output-Buffering gerade dann gar nicht braucht. Wir können es ja gleich richtig schreiben. Meistens wird es benutzt, wenn man keine Kontrolle über alle Vorgänge zwischen ob_start() und ob_get_clean() hat – und dennoch eine bestimmte Ausgabe erzwingen möchte.

Genau dieser Mangel an Kontrolle kann aber nicht durch PHP repariert werden – denn Output-Buffering darf man verschachteln. Wenn alles gut geht, sieht das beispielsweise so aus:

<?php
error_reporting( E_ALL | E_STRICT );
header( 'Content-Type: text/html;charset=utf-8' );
ob_start();
    ob_start();
    header( 'Content-Type: text/html;charset=utf-8' );
    print '<p>Das ist HTML!</p>';
    print ob_get_clean();
print '<p>Das ist kein HTML.</p>';
header( 'Content-Type: text/plain;charset=windows-1252' );
print ob_get_clean(); // text/plain

Ergebnis: <p>Das ist HTML!</p><p>Das ist kein HTML.</p>

Wenn es nicht gut geht, haben wir zwei Strings mit unterschiedlichen Zeichenkodierungen in der Ausgabe. In einem von beiden werden jetzt notwendig die Umlaute versaut. Und es wird noch schlimmer, wenn die Autoren beider Output-Buffer unkoordiniert Anfang und Ende festlegen und einander in die Quere kommen:

<?php
error_reporting( E_ALL | E_STRICT );
header( 'Content-Type: text/html;charset=utf-8' );
ob_start(); // Autor 1
print '<p>HTML. Soll ein schmucker Absatz werden.</p>';
    ob_start(); // Autor 2
    print '<p>Kein HTML. Muß sicherheitshalber maskiert werden.</p>';
print ob_get_clean(); // Autor 1
    print htmlspecialchars( ob_get_clean() ); // Autor 2

Den erstrebten Absatz kann sich Autor 1 jetzt in die Haare schmieren.

Dieses Problem haben wir bei allen Systemen, die durch Skripte erweitert werden können. Ich kenne es von WordPress, selbst aus meinen eigenen Plugins, bei denen ich zu optimistisch war. Und da für PHP alles akzeptabel aussieht, bekommt man hier keine brauchbare Fehlermeldung – das Debuggen macht dann noch weniger Spaß als sonst.

Daher mein Rat: Laßt es. Wenn es gar nicht anders geht und der einzufangende String wirklich sehr, sehr kurz ist, kann man es riskieren. In einem Styleguide aber würde ich es glatt verbieten. Es gibt fast immer bessere Wege.

3 Kommentare

  1. Sascha am 28.08.2012 · 18:26

    Jetzt wären die besseren Wege interessant. Und wann bzw. wieso sie fast besser sind.

  2. Thomas Scholz am 28.08.2012 · 19:31

    Das kommt immer auf die API des jeweiligen Systems an. Hier ist mal ein Beispiel für WordPress: How to get the result of comments_number() as a string instead of printing it out?

    Zwei unterschiedliche Wege zum selben Ziel. ob_start() ist hier einfacher zu lesen und – wenn man den eingefangenen Code kennt – auch einigermaßen sicher. Deshalb habe ich es als Antwort akzeptiert. Meine eigene Lösung für das Problem ist sicher, nur eben etwas schwerer zu verstehen. Sie wäre jedoch in anderen Fällen vorzuziehen – sofern ein passender Filter existiert.

  3. Ralf Albert am 29.08.2012 · 23:09

    So lange man noch eine (Fehler) Ausgabe hat, kann man auch debuggen. Richtig fies wird es aber spätestens dann, wenn man einen Fehler in einem Objekt hat.
    Tritt nach einem ob_start() in einem Objekt ein Fehler auf, kann es dazu kommen das PHP zwar den Puffer-Inhalt ausgibt, die Fehlermeldung jedoch nicht.

    Outputbufferig macht mir auch immer wieder Kopfschmerzen. Und ich wäre heilfroh, wenn man die Ausgabe vernünftig puffern könnte. Sprich, wenn man separate Puffer anlegen kann anstatt das alle auf einen Puffer zugreifen.