Archive for the ‘zend framework’ tag
Adress- und Kontaktdaten als VCard exportieren
VCards sind kleine Dateien, die Personen- und Adressbuch-Daten enthalten. Diese Dateien kann man wunderbar an Emails dranhängen, oder aber als Export + Import zwischen verschiedenen Programmen (Outlook, Thunderbird, Mac OS X Addressbook etc) nutzen. Als Premium-Mitglied bei Xing kann man alle seine Kontakte dort auch exportieren und in die oben genannten Applikationen oder auch in sein Handy importieren.
Wie sieht eine VCard aus? Ungefähr so:
BEGIN:VCARD VERSION:3.0 N:Kliewe;Michael FN:Michael Kliewe ADR;TYPE=HOME,WORK:;;Am Stadtgarten 8a;Oelde;;59302 EMAIL;TYPE=HOME;TYPE=WORK:emailadresse@domain.de EMAIL;TYPE=PREF:wichtige@emailaddresse.de END:VCARD
Man sieht, das Format ist schon recht alt (1998), heutzutage würde man wahrscheinlich XML wählen. Aber es ist nun mal DER Standard, wir wollen die Adressdaten aus unserer Applikation als VCard exportieren. Dazu gibt es bereits einige vorgefertigte Klassen (Contact_Vcard_Build, PHP vCard Class 2.0, class vcard), die ich aber hier nicht nutzen möchte, da sie entweder das ältere 2.1 Format nutzen oder kein UTF-8 unterstützen. Hier eine einfache Lösung mit wenigen Zeilen Code:
<?php header("Content-type:text/x-vCard; charset=utf-8"); header("Content-Disposition: attachment; filename=vcardexport.vcf"); $first = "Michael"; $last = "Kliewe"; $email = "emailadresse@domain.de"; ?> BEGIN:VCARD VERSION:2.1 N:<?php echo($last); ?>;<?php echo($first); ?> FN:<?php echo($first); ?> <?php echo($last); ?> EMAIL;PREF;INTERNET:<?php echo($email); ?> REV:<?php echo date('Y-m-d H:i:s'); ?> END:VCARD
Das ist natürlich nur der Anfang und ein erster Test. Aber das Prinzip ist recht einfach: Jede Zeile enthält eine Eigenschaft, wie „Voller Name“, „Email-Adresse“, „Wohnadresse“, „Notizen“, „Urls“, „Fotos“, „Firma“, „Geburtstag“ und so weiter. Key und Value sind durch einen Doppelpunkt getrennt, spezielle Optionen wie beispielsweise das oben sichtbare „PREF“ oder „TYPE“ sind via Semikolon getrennt hinzuzufügen. Weitere Informationen erhält man auf der englischen Wikipedia-Seite (die deutsche ist nicht so umfangreich). Man kann auch mehrere Kontakte in einer vcf-Datei exportieren, und objektorientiert mit dem Zend Framework sieht das ganze dann ungefähr so aus:
public function exportvcardAction() { $this->_helper->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(); $vcardString = ''; $contacts = $this->service->getAllContacts(); foreach ($contacts as $contact) { /* @var $contact App_Addressbook_Contact */ $vcardString .= $contact->getContactAsVcardString() . "\r\n"; } if (count($contacts) == 1) { $filename = str_replace(" ", "_", $contacts[0]->getFirstname().' '.$contacts[0]->getSurname()); } else { $filename = 'VCARDS'; } $this->getResponse()->setHeader('Content-Type', 'text/x-vCard; charset=utf-8'); $this->getResponse()->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'.vcf"'); $this->getResponse()->setHeader('Content-Length', strlen($vcardString)); $this->getResponse()->setHeader('Connection', 'close'); $this->getResponse()->setBody($vcardString); }
Damit werden nun also alle Kontakte iteriert und die einzelnen VCard-Strings aneinandergehängt. Wir nutzen hier die entsprechenden Funktionen, um das Layout und den ViewRenderer zu deaktiveren und alle Informationen ins Response-Objekt zu schreiben
Die Funktion getContactAsVcardString() habe ich so umgesetzt:
public function getContactAsVcardString() { $data['BEGIN'] = 'VCARD'; $data['VERSION'] = '3.0'; $data['PRODID'] = '-//company contact export Version 1//EN'; $data['REV'] = date('Y-m-d H:i:s'); $data['TZ'] = date('O'); $data['FN'] = $this->getDisplayName(); $data['N'] = $this->getSurname().';'.$this->getFirstname(); if ($this->getTitle() != '') { $data['TITLE'] = $this->getTitle(); } foreach ($this->getMailAddresses() as $mailAddress) { $data['EMAIL;TYPE=internet'] = $mailAddress; } if ($this->getNotice() != '') { $data['NOTE'] = $this->getNotice(); } $data['END'] = 'VCARD'; $exportString = ''; foreach ($data as $index => $value) { $exportString .= $index . ':' . $value . "\r\n"; } return $exportString; }
Als Ergebnis erhält man eine Datei zum Download mit allen $contacts incl. Details.
Zend_View Output Filter: Whitespaces aus HTML entfernen
Zend Framework implementiert das Intercepting-Filter-Pattern, und dadurch ist es sehr einfach nach der Generierung des HTML-Codes (Postprocessor) einen Filter einzufügen, der die Ausgabe noch verändert. Natürlich gibt es auch Präprozessor-Filter, in denen man häufig die Authentifizierung prüft oder Eingabeparameter validiert.
Vorstellen könnte man sich beispielsweise einen Filter, der bestimmte Textblöcke ersetzt, um zum Beispiel aus „mailto:“-Links Bilder zu generieren oder sie anders zu schützen, beispielsweise mittels Javascript.
Man kann sich auch einen Filter vorstellen, der das HTML-Ergebnis durch den HTML-Purifier schickt, ihn also W3C-kompatibel macht. Dies sollte man jedoch vermeiden wenn eben möglich und seinen HTML-Code auch vorher bereits korrekt generieren.
In diesem Artikel möchte ich einen ganz einfachen Filter einrichten, der alle Tabs, Leerzeichen und Umbrüche entfernt, sodass der komplette HTML-Code in einer Zeile steht und keine unnötigen Whitespaces enthält. Man kann damit den Traffic um ca. 3-15% (nicht gzip’t) verringern, nur durch das Entfernen von Tabs und Leerzeichen! Es ersetzt natürlich nicht die Aktivierung von gzip, um „richtig“ Traffic zu sparen und die Webseite schneller zu machen.
Wir schreiben uns also einen einfachen Filter, der die Aufgabe erfüllt:
<?php class App_View_Filter_Minify implements Zend_Filter_Interface { public function filter($string) { return preg_replace( array('/>\s+/', '/\s+</', '/[\r\n]+/'), array('>', '<', ' '), $string ); } }
Die filter() Methode bekommt den Ausgabestring übergeben, wir filtern alle Whitespaces heraus und geben das Ergebnis zurück. Wir sind hier etwas vorsichtig und filtern nur direkt vor oder nach HTML-Tags.
Diesen Filter müssen wir nun noch einbauen, und das geht wie folgt in der Bootstrap.php, wo wir im View-Objekt den Filter aktivieren:
/** * Add Output filters to View * * @return void */ protected function _initViewFilter() { $view = $this->getResource('view'); $view->addFilterPath('App/View/Filter', 'App_View_Filter_') ->addFilter('Minify'); }
Wir fügen dem View-Objekt also einen Pfad hinzu, wo wir den Filter abgelegt haben, und übergeben dann noch den Namen des Filters. Das Zend Framework wird dann nach der Abarbeitung der ControllerAction und dem Rendern des View-Scripts diesen Filter benutzen und die Ausgabe verändern.
Ergebnis ist dann eine einzelne Zeile HTML-Code ohne Whitespaces. Hier ein paar Ergebnisse dieses doch sehr einfachen Filters:
Größe vorher (KB) | Größe nachher (KB) | Ersparnis (%) |
---|---|---|
6,1 | 5,5 | 9,9 |
100,9 | 90,2 | 10,6 |
4,6 | 4,2 | 8,7 |
18,9 | 18,6 | 1,6 |
Vergleich der großen PHP-Frameworks: Anzahl Jobs
Wollte nur kurz einen Link in den Raum werfen, den man vielleicht mal länger beobachten kann, um zu sehen wie es mit den PHP Frameworks steht:
zend framework, cakephp, symfony, codeigniter Job Trends | zend framework jobs – cakephp jobs – symfony jobs – codeigniter jobs |
Das wars auch schon 😉
Taugt Zend_Queue etwas?
In einem älteren Artikel schrieb ich bereits über asynchrone Aufgaben. Heute möchte ich Zend_Queue vorstellen, womit man Aufgaben speichern kann, die später erledigt werden sollen.
Eine solche Message-Queue hat zwei Aufgaben: Aufgaben sollen erstellt werden können inklusive Details was zu tun ist, sowie Möglichkeiten bereitstellen, diese Aufgaben später wieder auslesen zu können, um sie abzuarbeiten.
Diese Aufgaben können wir nun in verschiedenen Backends speichern, Zend_Queue bietet einige Adapter an, wie beispielsweise eine Datenbank, ZendPlatformJobQueue, Apache ActiveMQ und MemcacheQ (bald wohl auch Amazon SQS).
Diese asynchrone Abarbeitung hat viele Vorteile. Für den Webseiten-Besucher erscheint die Webseite schneller, da einige Aufgaben später (wenige Sekunden, vielleicht auch sehr viel später) abgearbeitet werden. Für uns als Entwickler und Betreiber des Services besteht die Möglichkeit, Aufgaben in lastarmen Zeiträumen zu erledigen, sie auf mehrere Server aufzuteilen, die Last von den Frontendservern zu nehmen usw. Beim ZendPlatformJobQueue-Adapter kann man Jobs auch priorisieren, zu bestimmten Zeiten erledigen lassen, Abhängigkeiten zwischen den Jobs definieren und einiges mehr.
Mit einer Queue können wir auch regelmäßige Aufgaben abarbeiten lassen, beispielsweise das Aufräumen der Datenbank jede Nacht um 3 Uhr, pausieren von Aufgaben und spätere Wiederaufnahme der Tätigkeit, oder das Aufschieben von Aufgaben um x Stunden, beispielsweise soll immer exakt 2 Stunden nach dem Eintreffen eines Ereignisses eine Email versendet werden, wenn der Nutzer bis dahin nicht auf der Webseite war. Das geht allerdings wie gesagt nicht mit jedem Adapter.
Allgemein lässt sich sagen, dass wir alles offline abarbeiten sollten was nicht für ein Feedback des Besuchers benötigt wird.
Ein gutes Beispiel wäre ein Forum. Wenn ein Besucher einen neuen Beitrag schreibt, gibt es viel zu tun: Der Beitrag muss in der Datenbank gespeichert werden, der neue Inhalt muss in den Index gepumpt werden, Themenabonenten müssen benachrichtigt werden, RSS-Feeds müssen aktualisiert werden, und am Ende soll dem Benutzer sein fertiger Beitrag gezeigt werden. Das schreit doch nach Parallelität bzw. Queuing:
Wie funktioniert nun Zend_Queue? Hier ein einfaches Beispiel, wo wir eine Datenbank als Backend nutzen und einen Job eintragen. Zuerst müssen wir die Tabellen anlegen, die dazu benötigt werden. Das entsprechende SQL-Script befindet sich unter Zend/Queue/Adapter/Db/mysql.sql
Die entsprechenden PHP-Scripte zum Eintragen von Jobs und zum Auslesen von Jobs sehen so aus:
initQueue.php
<?php include('Zend/Loader/Autoloader.php'); $autoloader = Zend_Loader_Autoloader::getInstance(); $config = array( 'name' => 'myqueue', 'driverOptions' => array( 'host' => 'localhost', 'port' => '3306', 'username' => 'root', 'password' => '', 'dbname' => 'Queue', 'type' => 'pdo_mysql' ), 'options' => array( // use Zend_Db_Select for update, not all databases // can support this feature. Zend_Db_Select::FOR_UPDATE => true ) ); // Create a database queue $queue = new Zend_Queue('Db', $config); $queue->createQueue('myqueue');
insert.php
<?php require('initQueue.php'); $queue->send(serialize(array( 'action' => 'new article', 'articleId' => 13)) );
receive.php
<?php require('initQueue.php'); $messages = $queue->receive(5); foreach($messages as $msg) { $jobInfo = unserialize($msg->body); // do the work var_dump($jobInfo); $queue->deleteMessage($msg); }
Die Datenbank unterstützt nur einfache Jobs, es ist also nur für einfache Entkopplung zu gebrauchen, aber hier soll es als Beispiel reichen. Es werden aktuell keine Prioritäten, Ausführungszeiten oder ähnliches unterstützt, es ist eine reine Message-Queue. Der ZendPlatformJobQueue-Adapter unterstützt beispielsweise diese Features, sodass darauf zurückgegriffen werden sollte bei komplexeren Aufgaben. Soweit ich das bisher gelesen habe wird die Job Queue aber nur im kostenpflichtigen ZendServer enthalten sein, nicht in der Community Edition. Das ist echt schade, denn von den Features her ist das echt spitze was da geboten wird.
Wer also eine einfache Message-Queue benötigt, kommt mit Zend_Queue super klar, wer mehr braucht, sollte den ZendPlatformJobQueue-Adapter und den ZendServer nutzen (und vorher kaufen), oder sich bei Projekten wie Gearman oder Dropr umschauen, die ich in naher Zukunft auch noch vorstellen möchte.
Die eigene Suchmaschine in PHP leicht gemacht: Lucene
Nicht nur Google hat ausgereifte Suchalgorithmen, jeder Programmierer kann sich auch seine eigene Volltextsuche auf die Webseite bauen. Das können zum Beispiel alle Unterseiten sein, die durchsucht werden sollen, aber auch Dateien, Emails, Dokumente und Texte jeglicher Art.
Ich werde am Ende auch kurz aufzeigen, warum der Mysql-Volltextindex kein guter bzw. schneller Index ist, und warum Lucene und andere Suchengines ihre Daseinsberechtigung haben.
In diesem Artikel soll es also um Lucene gehen. Lucene ist ein Open-Source-Suchalgorithmus, der als Apache-Projekt weiterentwickelt wird und auf den viele weitere Produkte aufbauen (das bekannteste ist wohl Solr). Der Grundaufbau einer solchen „Suchmaschine“ besteht aus 2 Teilen: Dem Indexer und der Suche.
Der Indexer ist zum Befüllen des Datenbestandes (des Indexes) zuständig. Ihm übergibt man also alle Texte und Dokumente, und sagt ihm dabei, welche Felder und Daten davon wichtig sind, und eventuell noch wie wichtig die einzelnen Dokumente sind. Lucene ist zum Beispiel auch in der Lage, HTML-Dateien zu parsen und daraus title, meta-tags, header usw zu extrahieren. Man spart also Arbeit, und kann die Suche später auf die entsprechenden Bereiche beschränken. Der Index wird dann im Dateisystem abgelegt.
Die Suche spuckt dann die Ergebnisse aus, wenn man sie mit mehr oder minder komplexen Suchaufgaben befeuert. Dabei sind nicht nur einfache Stichwortsuchen möglich, sondern auch „ungefähre Treffer“, man erhält einen Relevanzwert(Score) und noch einige weitere Informationen.
Wenn wir nun in PHP einen solchen Index aufbauen wollen, nutzen wir am besten die Zend_Search_Lucene Klassen dafür. Hier ein einfaches Beispiel, wie man den Index füllt:
<?php include_once 'Zend/Loader.php'; Zend_Loader::registerAutoload(); $index = Zend_Search_Lucene::create('/tmp/index'); $document = new Zend_Search_Lucene_Document(); $document->addField(Zend_Search_Lucene_Field::Text('title', 'Titel 1 des Dokuments')); $document->addField(Zend_Search_Lucene_Field::Text('content', 'Hier ist ein toller Text')); $index->addDocument($document); $document = new Zend_Search_Lucene_Document(); $document->addField(Zend_Search_Lucene_Field::Text('title', 'Das hier ist der zweite Titel')); $document->addField(Zend_Search_Lucene_Field::Text('content', 'Und hier steht der Inhalt eines Buches')); $index->addDocument($document);
Wir definieren also ein Verzeichnis, in dem der Index abgelegt werden soll. Dann erstellen wir ein Dokument, zu dem wir dann ein Feld hinzufügen, in diesem Fall ein Textfeld. Dieses wird gesplittet und jedes Wort kann als Suchwort genutzt werden. Text-Felder werden zum Index hinzugefügt und komplett gespeichert, um sie bei den Ergebnissen auszugeben. Es gibt auch noch weitere Feldtypen, die zum beispiel nur indiziert aber nicht gespeichert werden, oder nur gespeichert und nicht indiziert. Hier gibt es eine Übersicht der Feldtypen.
Zum Schluss fügen wir das Dokument noch zum Index hinzu. Um die Suche nachher etwas interessanter zu machen, fügen wir noch ein weiteres Dokument hinzu. Das Ergebnis sieht dann so aus:
Reingucken brauchen wir da nicht, denn der Inhalt ist relativ kryptisch. Wir wollen ja auch nicht direkt auf diese Dateien zugreifen, sondern mittels der Suche. Das geht wie folgt:
<?php include_once 'Zend/Loader.php'; Zend_Loader::registerAutoload(); $index = Zend_Search_Lucene::open('/tmp/index'); $queries = array('Buch', 'toller', 'ist', 'title:ist'); foreach ($queries as $query) { $results = $index->find( Zend_Search_Lucene_Search_QueryParser::parse($query) ); echo "Suche: " . $query . "\n"; echo count($results) . " Ergebnisse \n\n"; foreach ($results as $result) { echo 'Inhalt: ' . $result->content . "\n"; echo 'Score: ' . $result->score . "\n"; echo "\n"; } }
Die Abfragen können normale Sucheworte sein, man kann nur in bestimmten Feldern suchen, boolsche Operatoren (AND/OR) nutzen als auch noch viel komplexere Abfragen starten.
Die Ausgabe sieht wie folgt aus:
Es ist also wirklich kein Hexenwerk, mit knapp 30 Zeilen haben wir sowohl den Index gefüllt als auch einige Suchabfragen gestartet und die Ergebnisse ausgegeben.
Mit Lucene kann man noch sehr viel mehr machen, alles hier aufzuzählen würde den Rahmen sprengen. Einfach mal im Zend Framework Manual gucken, dann bekommt man einen Eindruck, was alles möglich ist.
Achso, ich erwähnte ja noch, dass ein Mysql-Volltextindex nicht so sinnvoll ist. Sobald große Mengen an Daten anfallen, wird Mysql langsam. Hier gibt es ein wunderbares PDF-Dokument mit Benchmarks.
Interessant sieht auch Sphinx aus, habe mich allerdings noch nicht damit beschäftigt.
Lucene ist also besonders interessant bei Daten, die nicht bereits in der Datenbank vorhanden sind. Als Beispiele wären da Dokumente, Twitter-Nachrichten, Emails oder statische HTML-Dateien genannt. Bevor man das also in seine Datenbank pumpt, nur um eine langsame Volltextsuche zu erhalten, sollte man lieber Lucene benutzen.