Unit-testen von generierten PDFs mit PHPUnit und PDFBox

Zu den Features, die sich nicht leicht mit Unit Tests prüfen lassen gehört das generieren von PDF-Dateien.

Hilfreich dazu ist das Kommandozeilen-Tool PDFBox mit der Option ExtractText:

PDF

This application will extract all text from the given PDF document.

Das erlaubt uns schon mal, den Text-Inhalt des Dokuments zu testen oder nach bestimmten Strings darin zu suchen.

Interessant wird es mit der Option -html, die das PDF zu HTML konvertiert. Damit sind auch Struktur und Formatierungen ansatzweise testbar.

Leider arbeitet das Tool nicht mit Streams, wir müssen also temporäre Dateien nutzen. Ein einfaches Beispiel für eine Funktion, die ein PDF Dokument als String entgegennimmt, mit PdfBox in HTML konvertiert und das HTML als String zurückgibt:

/**
 * @var string $streamIn binary string with generated PDF
 * @return string HTML string
 */
function htmlFromPdf($streamIn)
{
  $pdf = tempnam();
  file_put_contents($pdf, $streamIn);
  $txt = tempnam();
  exec('java -jar pdfbox-app-x.y.z.jar ExtractText -encoding UTF-8 -html ' . $pdf . ' ' . $txt);
  $streamOut = file_get_contents($txt);
  unlink($pdf);
  unlink($txt);
  return $streamOut;
}

Für Regressionstests oder beim Refaktorisieren kann es mitunter ausreichen, zu testen, dass das generierte PDF sich gegenüber dem Referenz-Dokument nicht geändert hat. Hier bietet sich ein Hash an, die Datei selbst ist aber – vermutlich aufgrund von Timestamps – nicht bei jedem Durchlauf exakt gleich. Ein Hash des nach HTML konvertierten Dokuments dagegen genügt:

        // In PHPUnit test case:
        $converter = new PdfBox();
        $html = $converter->htmlFromPdfStream($pdf);
        $this->assertEquals('336edd9ee49b57e6dba5dc04602765056ce05b91', sha1($html), 'Hash of PDF content');

Continue reading “Unit-testen von generierten PDFs mit PHPUnit und PDFBox”

PHP: Referenzen und Speicher

Nutze niemals Referenzen in PHP, nur um Speicherbedarf zu reduzieren. PHP handhabt das bereits mit seinem internen copy on write Mechanismus.

Beispiel:

$a = str_repeat('x', 100000000); // Memory used ~ 100 MB
$b = $a;                         // Memory used ~ 100 MB
$b = $b . 'x';                   // Memory used ~ 200 MB

Du solltest Referenzen nur nutzen, wenn Du genau weißt, was du tust und sie für Funktionalität benötigst. Das ist fast nie der Fall, so dass man sie auch getrost völlig ignorieren kann. PHP-Referenzen sind im Allgemeinen eigenwillig und können in unerwartetem Verhalten resultieren.

Frage und Antwort auf StackOverflow

Große PHP-Arrays, SPL und Sessions

Folgende Problemstellung: eine große Datenmenge wird auf einmal abgefragt, soll aber nicht direkt komplett an den Client gesendet werden, also wird sie in der Session zwischengespeichert. Vielleicht im allgemeinen nicht die geschickteste Lösung, in meinem Fall fielen die Nachteile jedoch nicht ins Gewicht. “Groß” bedeutete dabei im Bereich von 10-50 MB in 50K-100K Datensätzen.

Das ist nun leider eine Menge, bei der PHP-Arrays nur noch mit Vorsicht einzusetzen sind. Der Flaschenhals war in diesem Fall array_shift(), womit Einträge aus dem in der Session befindlichen Arrays entnommen wurden. Was läge da näher, als auf eine der SPL-Datenstrukturen zurückzugreifen? Leider sind sowohl SplStack als auch SplFixedArray nicht serialisierbar und somit nicht ohne Weiteres mit Sessions zu gebrauchen.

Dies lässt sich nachrüsten, dabei muss allerdings doch wieder auf PHP-Arrays zurückgegriffen werden. Mit dem Performance-Verlust beim Serialisieren und Deserialisieren erkauft man sich allerdings eine deutlich effizientere Daten-Verarbeitung. In meinem Fall war SplStack bzw. SplDoublyLinkedList perfekt, da die Daten nur noch der Reihe nach abgeholt werden sollten. Die Erweiterung sieht wie folgt aus:

Serialisierbare SPL-Datenstruktur

class SerializableList extends SplDoublyLinkedList
{
    private $_data;
    
    public function __sleep()
    {
        $this->_data = array();
        
        $this->rewind();
        
        while ($this->valid()) {
            $this->_data[] = $this->current();
            $this->next();
        }
        
        return array('_data');
    }
    
    public function __wakeup()
    {
        foreach ($this->_data as $row) {
            $this->push($row);
        }
        
        $this->_data = array ();
    }
}

Kurz erkärt

Beim Serialisieren (__sleep()) wird die Datenstruktur in ein PHP-Array (im Attribut _data) konvertiert und mit return array('_data') festgelegt, dass genau dieses Attribut serialisiert werden soll. Beim Deserialisieren (__wakeup()) ist _data wiederhergestellt und kann zurück konvertiert werden. Anschließend wird mit $this->_data = array() der Speicher wieder freigegeben.

Vorsicht

Ob diese Lösung im konkreten Fall sinnvoll ist, kann nur durch eigene Messungen ermittelt werden. Dabei sollte vor allen Dingen darauf geachtet werden, die Anzahl der Serialisierungsvorgänge so gering wie möglich zu halten, denn wie schon gesagt sind die durch die zusätzliche Konvertierung teurer als zuvor. Als Beispiel: Bei 8 Abfragen a 10000 Datensätzen war meine Anwendung mindestens 20 mal schneller als bei 80 Abfragen a 1000 Datensätzen. Und beide Varianten schlagen die Implementierung nur mit PHP-Arrays um Längen.

Eine Herausforderung für weitere Optimierung wäre es noch, einen eigenen Serialisierer zu schreiben, der ohne PHP-Array auskommt. Das wäre allerdings eher etwas für die PECL, sprich direkt in C gehackt. In PHP selbst sehe ich da wenig Hoffnung in puncto Effizienz.

Propel 1.5.5: propel-gen schlägt fehl

Neuerdings bekam ich den Propel-Generator nicht mehr zum laufen, zunächst vermutete ich Konflikte wegen unterschiedlicher Versionen die gleichzeitig installiert waren aber auch ein Update und die Sicherstellung, nur mit der neuesten Version zu arbeiten half nicht.

Stellte sich heraus, dass Propel mit den neuesten Versionen von Phing nicht klar kommt, genau genommen allem nach 2.4.2. Es war also leider folgendes nötig:

pear install -f phing/phing-2.4.2

(-f ist der “force”-Parameter, um neuere Versionen zu überschreiben)

Und voilà, keine Fehlermeldungen mehr!

PHP: Undefined constant __COMPILER_HALT_OFFSET__

Diese Notice bekam ich gelegentlich in einer Datei mit __halt_compiler(). Es hat mich einige Zeit gekostet, bis ich das Problem entdeckt habe… der Fehler kam nur wenn ich die Seite innerhalb kurzer Zeit aktualisiert habe, so dass ich nach einer Weile den Opcode Cache als Ursache vermutete.

Tatsächlich hing es mit APC zusammen, ich habe folgenden Bug Report gefunden: http://pecl.php.net/bugs/bug.php?id=15788&edit=2

Lösung: APC updaten oder einfach andere Methoden nutzen, Daten zu speichern als am Ende eines PHP Skripts 😉

PHP-CLI Default-Werte für Kommandozeilenparameter

Heute mal ein nützliches Code-Snippet um Kommandozeilenparameter auszuwerten. Gegeben seien Standardwerte als Array in $conf['params'], beispielsweise

array(
	'narf' => 'zort',
	'foo' => false,
	'bar' => true,
);

Das Skript wollen wir so aufrufen, dass booleans als einwertige Parameter übergeben werden können:

# Standard:
php -f skript.php

# narf=puit, foo=true
php -f skript.php -- narf=puit foo

# bar=false
php -f skript.php -- bar=0

Und so wirds gemacht:

// CLI args override conf['params']
if (isset($argv)) {
	for($i=1;$i<$argc;++$i) {
		list($param,$value) = explode('=', $argv[$i], 2) + array(1=>true);
		$conf['params'][$param] = $value;
	}
}

Der Clou ist die Array-Vereinigung mittels +, der Index 1 wird hier im Gegensatz zu array_merge() nur gesetzt wenn er noch nicht vorhanden ist, somit wird foo behandelt wie foo=true

PHP Array Path

Hier mal ein kleines Snippet 1 um auf ein bestimmtes Element in verschachtelten Arrays zuzugreifen. Nützlich, wenn ein String der Form "key1.key2.key3" vorliegt, und damit auf $array['key1']['key2']['key3'] zugegriffen werden soll.

Funktionen

<?php
/**
  * @param string $spec Spezifikation in der Form 'item_1.item_2.[...].item_n=wert'
  * @param array $array Ziel-Array
  */
function insert_into_array($spec, &$array)
{
    list($path,$value) = explode('=', $spec, 2);
    $current =& $array;
    // setze Referenz $current Schritt für Schritt auf $array['item_1']['item_2'][...]['item_n']
    foreach(explode('.', $path) as $key) {
        $current =& $current[$key];
    }
    // belege dieses Array-Element mit $value
    $current = $value;
}  
/**
 * @param string $path Pfad in der Form 'item_1.item_2.[...].item_n'
 * @param array $array Ursprungs-Array
 */
function &get_from_array($path, &$array)
{
    $current =& $array;
    foreach(explode('.', $path) as $key) {
        $current =& $current[$key];
    }
    return $current;
}

Beispiel: Nutzung

$array = array();
insert_into_array('item.test.6.12134.12.12.343=4546', $array);
insert_into_array('item.test.23=foo', $array);
var_dump(get_from_array('item.test', $array));

Beispiel: Ausgabe

array(2) {
  [6] =>
  array(1) {
    [12134] =>
    array(1) {
      [12] =>
      array(1) {
        ...
      }
    }
  }
  [23] =>
  string(3) "foo"
}

Notes:

  1. Ausgehend von dieser Forumsdiskussion

PHP Iterator, IteratorAggregate mit current(), next() etc. nutzen

Einige Anmerkungen zu PHP Traversables

Iteration mit Funktionen

Traversables können mit foreach iteriert werden, jedoch nicht im allgemeinen mit Array-Iterations Funktionen wie reset() und next().

Iterator Objekte können auch auch mit folgenden Funktionen verwendet werden:

  • current()
  • next()
  • prev()
  • reset()
  • end()

Achtung: Es ist zwar möglich, allen diesen Funktionen IteratorAggregate Objekte zu übergeben (wie auch jedes andere Objekt), die Iterator-Funktionalität wird dabei aber nicht genutzt, sondern es wird über die Attribute des Objekts iteriert.

Das selbe gilt für Iterator Objekte mit each()! Dieses sollte generell nicht für Objekte benutzt werden, da hilft weder ArrayAccess noch Iterator. Es funktioniert im allgemeinen nie wie erwünscht.

Überprüfung von unbekannter Variable auf Iterierbarkeit mit foreach

$a implements Traversable || is_array($a);
  • Traversable ist vor Iterator zu bevorzugen da auch IteratorAggregate und andere interne Iteratoren damit erkannt werden.
  • Arrays sind keine Traversables, da keine Objekte

Will man allerdings volle Flexibilität genügt:

is_object($a) || is_array($a)
  • PHP kann beliebige Objekte als Iterator benutzen und iteriert dabei über die öffentlichen Attribute!