Text
                    e-bol.net
Carsten Möhrke
Zend Framework
Das Entwickler-Handbuch
Galileo Press

e-bol.net Liebe Leserin, lieber Leser, mit dem neuen Buch von Carsten Möhrke haben Sie alles an der Hand, was Sie für den professionellen Einsatz des Zend Framework benötigen. Vielen dürfte unser Autor bereits durch seine erfolgreichen Bücher zu PHP PEAR und vor allem durch sein »Besser PHP programmieren«-Buch bekannt sein. Durch die enge Verknüp- fung von Theorie- und Praxiswissen garantiert Carsten Möhrke, dass Sie Gelerntes immer unmittelbar in Ihren Projekten umsetzen können. Das Zend Framework hat innerhalb kurzer Zeit bereits viele überzeugte Anhänger gefunden. Vieles spricht für den Einsatz des Frameworks bei der Entwicklung kom- plexer und sicherheitskritischer Aufgaben. So können Sie sich auf die eigentliche Applikationsstruktur konzentrieren, haben ausgereiften Quellcode zur Verfügung und sparen so letztendlich deutlich Zeit. Carsten Möhrke begleitet Sie auf diesem Weg und zeigt Ihnen, wie Sie das Model View Controller-Muster nutzen oder die zahlreichen Funktionen des Zend Frameworks verwenden. Angefangen von der Installation über den Umgang mit Datenbanken, der Benutzerverwaltung, Perfor- mance-Fragen, der E-Mail-Implementierung oder dem Einsatz von Webservices: Dieses Buch zeigt Ihnen, welche Möglichkeiten das Zend Framework bietet und wie Sie es für sich nutzen können. Ein besonderer Dank geht an dieser Stelle an Gaylord Aulke von der Zend Techno- logies GmbH. Er hat das Manuskript in seiner letzten Phase einer umfassenden Prü- fung unterzogen und das Buch so kompetent begleitet. Um die Qualität unserer Bücher zu gewährleisten, stellen wir stets hohe Ansprüche an Autoren und Lektorat. Falls Sie dennoch Anmerkungen und Vorschläge zu die- sem Buch formulieren möchten, so freue ich mich über Ihre Rückmeldung. Ihr Stephan Mattescheck Lektorat Galileo Computing stephan.mattescheck@galileo-press.de www.galileocomputing.de Galileo Press • Rheinwerkallee 4 • 53227 Bonn
e-bol.net Auf einen Blick 1 Der Model View Controller.............................. 25 2 Datenbankzugriff mit Zend_Db .......................... 59 3 Benutzer-und Rechtemanagement ........................ 115 4 Infrastruktur-Klassen ................................ 137 5 Webservices........................................... 209 6 Arbeit mit E-Mails und Dateiformaten ................. 279 7 Protokolle und Co..................................... 335 8 Lokalisierung und Internationalisierung .............. 369
e-bol.net Der Name Galileo Press geht auf den italienischen Mathematiker und Philosophen Galileo Galilei (1564-1642) zurück. Er gilt als Gründungsfigur der neuzeitlichen Wissenschaft und wurde berühmt als Verfechter des modernen, heliozentrischen Weltbilds. Legendär ist sein Ausspruch Eppur se muove (Und sie bewegt sich doch). Das Emblem von Galileo Press ist der Jupiter, umkreist von den vier Galileischen Monden. Galilei entdeckte die nach ihm benannten Monde 1610. Gerne stehen wir Ihnen mit Rat und Tat zur Seite: stephan.mattescheck@galileo-press.de bei Fragen und Anmerkungen zum Inhalt des Buches service@galileo-press.de für versandkostenfreie Bestellungen und Reklamationen stefan.krumbiegel@galileo-press.de für Rezensions- und Schulungsexemplare Lektorat Stephan Mattescheck Fachgutachten Gaylord Aulke, Zend Technologies Korrektorat Rene Wiegand, Bonn Cover Barbara Thoben, Köln Titelbild Corbis Typografie und Layout Vera Brauner Herstellung Katrin Müller Satz Typographie & Computer, Krefeld Druck und Bindung Bercker Graphischer Betrieb, Kevelaer Dieses Buch wurde gesetzt aus der Linotype Syntax Serif (9,25/13,25 pt) in FrameMaker. Bibliografische Information der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://www.d-nb.de abrufbar. ISBN 978-3-8362-1068-3 © Galileo Press, Bonn 2008 1. Auflage 2008 Das vorliegende Werk ist in all seinen Teilen urheberrechtlich geschützt. Alle Rechte vorbehalten, insbesondere das Recht der Übersetzung, des Vortrags, der Reproduktion, der Vervielfältigung auf fotomechanischem oder anderen Wegen und der Speicherung in elektronischen Medien. Ungeachtet der Sorgfalt, die auf die Erstellung von Text, Abbildungen und Program- men verwendet wurde, können weder Verlag noch Autor, Herausgeber oder Übersetzer für mögliche Fehler und deren Fol- gen eine juristische Verantwortung oder irgendeine Haftung übernehmen. Die in diesem Werk wiedergegebenen Gebrauchs- namen, Handelsnamen, Warenbezeichnungen usw. können auch ohne besondere Kennzeichnung Marken sein und als solche den gesetzlichen Bestimmungen unterliegen.
e-bol.net Inhalt Geleitwort des Fachgutachters.............................................. 11 Einleitung ................................................................. 13 1 Der Model View Controller...................................... 25 1.1 Die Theorie des MVC.............................................. 25 1.2 Die Praxis des MVC .............................................. 26 1.2.1 Der Front Controller.................................... 29 1.2.2 Der Action Controller................................... 30 1.2.3 Einbinden eines Views .................................. 34 1.2.4 Die Verarbeitungsschritte .............................. 37 1.2.5 Übergabe von Werten .................................... 38 1.2.6 Das Model .............................................. 41 1.2.7 Error Handling ......................................... 42 1.2.8 Fortgeschrittene Techniken ............................. 47 1.2.9 Ein Beispiel ........................................... 50 2 Datenbankzugriff mit Zend Db ............................ 59 2.1 Datenbankunabhängigkeit ..................................... 59 2.2 Nutzung von Zend_Db ......................................... 60 2.2.1 Transaktionen ....................................... 73 2.2.2 Sequenzen und automatisch generierte IDs ............ 75 2.2.3 Spezielle Datenbankzugriffsmethoden ................. 76 2.3 Datenbankzugriff mit Zend_Db_Table .......................... 88 2.3.1 Einfügen von Daten .................................. 93 2.3.2 Aktualisieren von Daten ............................. 94 2.3.3 Löschen von Daten ................................... 95 2.3.4 Auslesen von Daten .................................. 95 2.3.5 Kaskadierende Lösch- und Update-Vorgänge ........... 109 2.4 Performanceanalyse mit Zend_Db_Profiler .................... 110 3 Benutzer- und Rechtemanagement ...................115 3.1 Rechteverwaltung mit Zend_Acl ............................ 115 3.1.1 Vererbung von Rechten ............................ 118 3.1.2 Verfeinern des Rechtesystems ..................... 120 3.1.3 Manipulieren von Rechten ......................... 123 5
e-bol.net Inhalt 3.2 Benutzerauthentifikation mit Zend_Auth ....................... 124 3.2.1 Datenbankbasierte Authentifikation .................. 124 3.2.2 Dateibasierte HTTP-Authentifikation ................. 128 3.3 Session-Verwaltung mithilfe von Zend_Session ................. 129 3.3.1 Eine Session starten ................................ 130 3.3.2 Gültigkeit und Schutz von Session-Daten ............. 132 3.3.3 Nutzung eigener Session-Save-Handler ................ 133 4 Infrastruktur-Klassen.........................................137 4.1 Performance-Optimierung mit Zend_Cache ....................... 137 4.1.1 Frontends ........................................... 139 4.1.2 Nutzung von Backends................................. 149 4.1.3 Manuelle Verwaltung von Cache-Einträgen ............. 153 4.2 Prüfen von Werten mit Zend_Validate .......................... 154 4.2.1 Prüfen auf alphanumerische Daten .................... 156 4.2.2 Prüfen von Texten ................................... 156 4.2.3 Prüfen, ob eine Zahl in einem bestimmten Bereich liegt................................................. 156 4.2.4 Prüfen von Kreditkartennummern ...................... 157 4.2.5 Prüfen eines Datums ................................. 157 4.2.6 Testen von Ziffernfolgen ............................ 157 4.2.7 Validieren von E-Mail-Adressen ...................... 157 4.2.8 Testen eines Strings auf Fließkomma-Eigenschaften.... 160 4.2.9 Prüfen, ob eine Zahl über einer Grenze liegt......... 160 4.2.10 Testen von hexadezimalen Zahlen ..................... 160 4.2.11 Validieren von Hostnames............................. 161 4.2.12 Testen von Array-Inhalten ........................... 163 4.2.13 Validieren von Integer-Werten ....................... 163 4.2.14 Prüfen von IP-Adressen .............................. 163 4.2.15 Prüfen, ob eine Zahl unter einer Grenze liegt ....... 164 4.2.16 Testen, ob eine Variable leer ist ................... 164 4.2.17 Validierung auf Basis eines regulären Ausdrucks ..... 164 4.2.18 Testen eines Strings auf seine Länge hin ............ 165 4.3 Filtern von Daten mit Zend_Filter ............................ 165 4.3.1 Alphanumerische Zeichen mit Zend_Filter_Alnum filtern ............................................. 167 4.3.2 Buchstaben filtern .................................. 168 4.3.3 Extrahieren eines Basenames.......................... 168 4.3.4 Ziffern mit Zend_Filter_Digits ausfiltern ........... 169 6
e-bol.net Inhalt 4.3.5 Extrahieren von Verzeichnisnamen ................... 169 4.3.6 Konvertieren von Sonderzeichen in Entitäten ........ 169 4.3.7 Filtern von Integer-Werten ......................... 170 4.3.8 Absolute Pfade mit Zend_Filter_RealPath extrahieren .... 170 4.3.9 Konvertieren in Kleinbuchstaben .................... 170 4.3.10 Konvertieren in Großbuchstaben .................... 171 4.3.11 Entfernen von Whitespaces .......................... 171 4.3.12 Entfernen von HTML-Tags............................. 172 4.3.13 Nutzung eigener Filter ............................. 174 4.4 Formularverarbeitung mit Zend_Filter_lnput .................. 175 4.5 Schreiben von Logs mit Zend_Log.............................. 184 4.5.1 Log-Einträge filtern ............................... 190 4.5.2 Logfile-Einträge formatieren ....................... 191 4.5.3 Eigene Einträge definieren.......................... 194 4.6 Konfigurationsverwaltung Zend_Config......................... 194 4.6.1 Nutzung von Konfigurations-Arrays................... 195 4.6.2 INI-Dateien......................................... 197 4.6.3 XML-Dateien ........................................ 200 4.7 Shell-Programmierung mit Zend_Console_Getopt ................ 203 4.7.1 Optionen, Flags und Parameter....................... 204 4.7.2 Nutzung von Argumenten ............................. 207 5 Webservices ..........................................209 5.1 Feeds mit Zend_Feed verarbeiten ............................ 209 5.1.1 Feeds finden ....................................... 209 5.1.2 Allgemeines zur Verarbeitung von Feeds ............ 210 5.1.3 Verarbeiten von RSS-Feeds ......................... 211 5.1.4 Verarbeiten von Atom-Feeds ........................ 214 5.1.5 Generieren von Feeds .............................. 218 5.2 Zugriff auf Amazon mit Zend_Service_Amazon ................. 220 5.3 Zugriff auf Flickr mit Zend_Service_Flickr ................. 232 5.4 Yahool-Suche mit Zend_Service_Yahoo ........................ 236 5.4.1 Websuche ........................................... 236 5.4.2 News-Suche mit Zend_Service_Yahoo .................. 240 5.4.3 Bildersuche mit Zend_Service_Yahoo ................. 242 5.5 Zugriff auf Google-Dienste mit Zend_Gdata .................. 245 5.5.1 Allgemeines zu Zend_Gdata .......................... 246 5.5.2 Authentifikation ................................... 246 5.5.3 Nutzung von Google Calendar......................... 255 5.5.4 Nutzung von Google Spreadsheets .................... 271 7
e-bol.net Inhalt 6 Arbeit mit E-Mails und Dateiformaten....................279 6.1 E-Mails mit Zend_Mail verarbeiten ........................ 279 6.1.1 E-Mails versenden................................. 280 6.1.2 Versand über SMTP-Server.......................... 286 6.1.3 E-Mails abholen .................................. 288 6.1.4 Löschen von E-Mails .............................. 298 6.1.5 Erweiterte Möglichkeiten von IMAP ................ 300 6.2 JSON-Daten mit Zendjson verarbeiten ...................... 306 6.3 Generieren von PDF-Dokumenten ............................ 308 6.3.1 Nutzung anderer Schriften ........................ 313 6.3.2 Mehrzeilige Fließtexte............................ 315 6.3.3 Nutzung von Farben ............................... 320 6.3.4 Zeichnen in PDF-Dokumenten ....................... 322 6.3.5 Einbinden von Bildern ............................ 330 6.3.6 Meta-Information en einfügen ..................... 331 6.3.7 Einlesen von PDF-Dokumenten....................... 333 7 Protokolle und Co........................................... 335 7.1 Zugriff auf andere Server mit Zend_Http ................... 335 7.1.1 Das HTTP-Protokoll ................................ 335 7.1.2 Einen HTTP-Client erstellen........................ 336 7.1.3 Übergabe von Werten ............................... 338 7.1.4 Uploads ........................................... 340 7.1.5 HTTP-Authentifikation ............................. 341 7.1.6 Server-Antworten auswerten......................... 342 7.1.7 Cookies ........................................... 346 7.1.8 Nutzung von Adaptern .............................. 351 7.2 URIs mit ZendJJri verarbeiten.............................. 352 7.2.1 URIs analysieren................................... 353 7.3 Nutzung von XML-RPC mit Zend_XmlRpc ....................... 355 7.3.1 Allgemeines zu Zend_XmlRpc ........................ 355 7.3.2 Erstellen eines XML-RPC-Servers.................... 356 7.3.3 Erstellen eines XML-RPC-Clients ................... 359 7.4 Nutzung von REST mitZend_Rest ............................. 361 7.4.1 Zugriff auf offene REST-Schnittstellen ............ 362 7.4.2 Implementation eines REST-Servers ................. 363 8
e-bol.net Inhalt 8 Lokalisierung und Internationalisierung........................369 8.1 Lokalisierung mit Zend_Locale .............................. 369 8.1.1 Standardtexte und Standardformate lokalisieren...... 371 8.2 Mehrsprachige Oberflächen mit Zend_Translate ............... 381 8.2.1 Nutzung von CSV-Dateien ............................ 384 8.3 Konvertieren von und Rechnen mit Maßeinheiten mittels Zend_Measure ..................................................... 385 8.4 Währungsdarstellung mit Zend_Currency ...................... 389 8.5 Datums-und Zeitangaben mit Zend_Date verarbeiten ........... 395 8.5.1 Ableiten eines Zend_Date-Objekts ................... 395 8.5.2 Rechnen mit Daten .................................. 402 8.5.3 Vergleich von Daten ................................ 404 8.5.4 Prüfen von Datumsinformationen ..................... 407 Inhalt der CD-ROM .................................................................. 409 Index 411 9
e-bol.net
e-bol.net Geleitwort des Fachgutachters Wie schön war die Welt 1997. Und wie einfach. Wir schrieben unsere Websites (damals hießen sie noch nicht »Anwendungen«) und waren fasziniert davon, wenn der Server überhaupt etwas auslieferte. Ich kann mich noch gut an die Revolution des ersten animierten GIF erinnern: ein Briefumschlag, in dem ein kleiner Brief verschwand. Immer und immer wieder! Unsere Vision damals: Webseiten, die automatisch generiert werden und ohne Technikkenntnisse ver- waltbar wären! Damals schon in Java, Perl und neuerdings auch in PHP betrie- ben. Seitdem hat sich einiges getan. Die New Economy kam und ging, aus Inter- net wurde Web 2.0 und das Web ist vom Spielplatz der Freaks zu einer wichtigen Wirtschaftsplattform geworden. Und aus PHP/FI wurde PHP 5. Nachdem bis PHP 4 so ziemlich jedes Team seine Erfahrungen in der PHP-Programmierung in ein eigenes Framework gegossen hatte, wurden parallel zu Version 5 neue Her- ausforderungen sichtbar wie: Content Syndication, Ajax, Security. Für viele kam zusätzlich erstmals auch die Forderung nach modularen, wartbaren Anwendun- gen auf, die in einem professionellen Teamansatz betreut und weiterentwickelt werden konnten. Gleichzeitig wurden mit PHP 5 die Sprachkonstrukte einge- führt, die Generalisierung und Modularisierung weit besser unterstützen, als dies bisher der Fall war. Was also tun? Das PHP 4-Framework einmotten und ein neues in PHP 5 aufbauen? Viele machen genau das. Aber glücklicherweise ent- scheiden sich immer mehr Teams zur Nutzung von einem der mittlerweile über 50 Open-Source-PHP-Frameworks. Statt Energie in einen Eigenbau zu investie- ren, helfen sie beim weiteren Ausbau eines Standard-Frameworks. Das Zend Framework ist angetreten, einer möglichst breiten Nutzerschaft eine Basis für moderne, feature-reiche, sichere und zukunftssichere Webanwendun- gen in PHP zu bieten. Dabei bewegt man auf dem schmalen Grad zwischen sinn- voller Generalisierung und der Komplexität von üblichen Java-Anwendungen (die man ja in PHP bewusst nicht will). Bereits jetzt, kurz nach Erscheinen der 1.0 Version zeichnet sich ab, dass mit Zend Framework nicht nur ein nützlicher Bau- kasten von Komponenten ausgeliefert wird, sondern dass sich hier »best practices« der PHP-Programmierung niederschlagen und De-facto-Standards im PHP Sektor entstehen. Das Zend Framework ist durch seine konzeptionelle Offenheit und die breite Akzeptanz zu einem der wichtigsten Faktoren moderner Anwendungsentwicklung in PHP geworden. Das vorliegende Buch fasst die grundlegenden Konzepte des Zend Frameworks zusammen und beleuchtet die Möglichkeiten, die sich aus den unterschiedlichen Komponenten ergeben. Es bie- tet damit einen guten Einstieg in das Framework und die damit verbundenen Programmierpraktiken. Ich empfehle Ihnen dieses Buch, wenn Sie bisher noch 11
e-bol.net Geleitwort des Fachgutachters nicht mit dem Zend Framework gearbeitet haben und eine solide Einführung wünschen. Aber auch, wenn Sie bereits erste Erfahrungen mit dem Zend Frame- work gemacht haben, wird Ihnen das Buch von Carsten Möhrke als wertvolles Nachschlagewerk in der praktischen Entwicklungsarbeit dienen. Gaylord Aulke Zend Technologies GmbH 12
e-bol.net You never win a game unlessyou beat the guy in front ofyou. The score on the board doesn't mean a thing. That'sfor the fans. You've got to win the war with the man in front ofyou. You've got to getyour man. - Vince Lombardi, Football Coach Einleitung Zend Framework wurde lange und mit viel Spannung erwartet. Nach einigen Monaten Entwicklungszeit ist die erste Version am 30. Juni 2007 veröffentlicht worden. Neben Zend Framework existieren auch viele andere etablierte Frameworks am Markt. Warum sollte man also Zend Framework nutzen? Meiner Ansicht nach gibt es eine ganze Reihe Gründe, dies zu tun. Zuerst einmal ist da natürlich die hohe Qualität des Codes. Der Name Zend bürgt für Qualität. Neben einer robusten Implementation sind auch viele Ansätze des modernen Software Engineerings zu finden. Die Entwicklung ist komplett testge- trieben, sodass die Anzahl der Bugs eher gering ist. Natürlich gibt es hier und da mal einen Bug, wie in jeder anderen Software auch, aber in den meisten Fällen sind diese wenig dramatisch und werden schnell korrigiert. Selbstverständlich gibt es Coding Standards und Namenskonventionen an die sich alle Entwickler halten. Ich würde Ihnen empfehlen, dass Sie diese bei der Entwicklung Ihrer Anwendungen auch zu Grunde legen. Sie können diese hier nachlesen: http://framework.zend.com/manual/coding-standard.html. In den Reihen des Entwicklerteams finden Sie viele Entwickler, die sich in der PHP-Szene seit vielen Jahren einen Namen gemacht haben, erfahren sind und hochwertigen Code erstellen. Für die Nutzung spricht auch das interessante Lizenz-Modell (http://framework. zend.com/license). Es ist wesentlich freier und flexibler als viele andere Lizenzen. Es ist auch ohne Probleme möglich, Code der auf dem Zend Framework basiert kommerziell zu vertreiben. Ein weiterer Punkt ist die wirklich gute Implementation des Model View Con- trollers (MVC), der Ihnen gerade bei umfangreichen Applikationen sehr viel 13
e-bol.net Einleitung Arbeit abnehmen kann, zu einem deutlich strukturierten Code führt und sehr robust umgesetzt ist. Aber keine Angst, Sie sind nicht gezwungen auf MVC aufzusetzen. Jede Kompo- nente aus dem Zend Framework kann einzeln genutzt werden, sodass Sie voll- kommen flexibel sind. Eine besondere Stärke des Zend Frameworks sind sicherlich die Klassen zur Internationalisierung bzw. Lokalisierung von Code. Hier finden sich viele sehr leistungsfähige Klassen, die Sie gut unterstützen, wenn Sie mehrsprachige Anwendungen für eine internationale Zielgruppe erstellen. Auch die Klassen, die Ihre Applikation »im Hintergrund« unterstützen, den Zugriff auf eine Datenbank ermöglichen, Daten cachen, Logs schreiben oder E-Mails verschicken, sind wirk- lich sehr umfangreich, performant und stabil. Aber genug des Lobes. Ich möchte nicht verschweigen, dass das Zend Framework auch noch ein paar deutliche Schwächen hat. So könnte beispielsweise die eine oder andere Webservice-Klasse mehr Funktionalitäten gebrauchen, und auch die PDF-Klasse ist noch ein wenig schwach auf der Brust. Dennoch sind auch diese Klassen auf einem guten Weg. Sollten Sie zum jetzigen Zeitpunkt allerdings eine dieser Klassen nutzen wollen, so ist es empfehlenswert, vorher zu prüfen, ob die benötigten Funktionalitäten enthalten sind. Einen Punkt - und das ist meiner Ansicht nach ein absolut unschlagbarer Vorteil - gibt es aber noch. Und zwar finden Sie in der Zend-IDE Zend Studio for Eclipse eine komplette Syntaxunterstützung für das Zend Framework. Das heißt, die IDE kennt alle Klassen und Methoden und schlägt Ihnen diese bei der Code Comple- tion vor. Damit aber nicht genug. Möchten Sie ein neues Projekt anlegen, so kön- nen Sie dem Tool auch von vornherein mitteilen, dass Sie ein neues MVC-Projekt auf Basis des Zend Frameworks anlegen wollen. Das Zend Studio generiert dann den kompletten »Standard-Code«, damit Sie ihn nicht tippen müssen. Sie können sich sicher vorstellen, dass Sie Ihren Code mit diesen Funktionalitäten deutlich schneller entwickeln können. Arbeiten Sie professionell mit PHP, so sollten Sie auf jeden Fall einen Blick auf Zend Studio for Eclipse werfen. Die Investition lohnt sich. Informationsquellen Zwar hoffe ich als Autor natürlich, dass das Buch, das Sie gerade in Händen hal- ten, die meisten Fragen zum Zend Framework beantworten wird. Aber ich weiß natürlich, dass noch Fragen offenbleiben. Bestimmt werden Sie noch eine Frage zu einem Paket haben, das hier nicht erläutert wird. Oder es gibt eine Änderung 14
e-bol.net Einleitung im Framework, die dazu führt, dass das Buch nicht mehr ganz aktuell ist. Wo können Sie dann weitere Informationen erhalten? Zum Ersten ist natürlich die Website des Zend Frameworks eine gute Anlauf- stelle. Dort finden Sie eine sehr brauchbare Dokumentation vor. Als Program- mierer werden Sie das Problem kennen, dass man nicht gerne dokumentiert. Aber man muss wirklich sagen, dass das Manual des Zend Frameworks eine rühmliche Ausnahme darstellt. Die Dokumentation finden Sie unter der Internet- adresse http://framework. zend. com/manual/. Sie sollten hierbei die englische Variante der Dokumentation bevorzugen, weil sie in Teilen einfach aktueller ist, und sich bei anderen Sprachen bei der Überset- zung schon mal ein Fehler einschleichen kann. Da Programmierer lieber programmieren als schreiben, kann es auch sehr hilf- reich sein, sich die Demos anzuschauen, die für einige Pakete vorhanden sind. Diese finden Sie im Download-Archiv des Zend Frameworks im Ordner demos. Auf der Website des Zend Frameworks sehen Sie oben in der Navigation auch einen Link, der mit »Support« beschriftet ist. Hier finden Sie einige Möglichkei- ten, wie Sie Unterstützung erhalten oder sich mit anderen Anwendern austau- schen können. Sehr interessant ist sicherlich auch das Wiki zum Zend Frame- work, auf dem Sie eine ganze Menge an Hintergrundinformationen finden. Sie erreichen es unter http://framework.zend.com/wiki/display/ZFDEV/Home. Im Support-Bereich finden Sie einen Link, unter dem Sie sich für diverse Mailing- Listen anmelden können. Diese Listen sind, auch wenn sie englischsprachig sind, sehr zu empfehlen. Zum einen sind diese Listen wirklich sehr aktiv und werden von vielen Benutzern abonniert. Zum anderen sind auch viele Entwickler der Pakete sehr aktiv. Das heißt, Sie bekommen oft sehr schnell eine qualifizierte Antwort. Es lohnt sich also, die Listen zu abonnieren. Sollten Sie eine Frage haben, sollten Sie aber zuvor vielleicht das Archiv der Mailing-Listen durchfors- ten, ob die Frage nicht schon einmal gestellt wurde. Auf der Support-Seite finden Sie zudem Links zu mehreren Blogs. Auch wenn Sie nach dem Zend Framework googeln, finden Sie schnell einige Blogs sowie auch Beiträge in Foren. Bitte genießen Sie solche Informationen immer mit Vorsicht. Gerade in der Zeit vor der Version 1.0 des Zend Frameworks haben sich noch viele Änderungen ergeben, die dazu führen, dass die Informationen in den Blogs veraltet sind. Sofern Sie eine solche Informationsquelle nutzen, so prüfen Sie bitte zunächst, wie alt der Beitrag ist. Gleiches gilt auch für viele Tutorials, die Sie in verschiedenster Form im Internet finden. 15
e-bol.net Einleitung Ein weiterer guter Anlaufpunkt ist die Website der Firma Zend selbst, die Sie unter www.zend.de erreichen. Dort finden Sie neben einigen Artikeln und Tuto- rials vor allem einige Webinare. Bei Webinaren handelt es sich um Präsentatio- nen bzw. Schulungen, die Sie im Browser betrachten können. Teilweise sind sie aufgezeichnet, sodass Sie sie jederzeit betrachten können, teilweise sind sie aber auch live. Ich denke, dass Webinare einen guten und vor allem entspannten Ein- stieg in ein Thema liefern können. Daher möchte ich sie Ihnen empfehlen. Neben den Angeboten, die in engem Zusammenhang mit Zend stehen oder von Zend stammen, gibt es natürlich noch andere Informationsquellen im Web. Inte- ressant ist sicher die Website ZFTutorials, die Sie unter der Adressse http://www.zfiutorials.com erreichen. Hier werden Tutorials rund um das Zend Framework gesammelt. Auch hier gilt, dass die Daten veraltet ein können. Eine zweite Website, die ich Ihnen empfehlen möchte, ist das deutschsprachige Zend Framework-Forum, das Sie unter http://www.z_fiorum.de finden. Hier sind sehr engagierte Mitglieder anzutreffen, die teilweise auch zu den Entwicklern des Zend Frameworks gehören, wodurch eine qualitativ hochwertige Hilfe sicherge- stellt ist. Neben den bisher vorgestellten Informationsquellen gibt es noch zwei Anlauf- stellen, die ich ebenfalls für sehr wichtig halte. Die erste ist der Zend Framework Issue Tracker. Dabei handelt es sich um das System, das für das Bug-Reporting genutzt wird. Wenn Sie also einmal ein Problem haben, dann kann es sich durch- aus lohnen, hier nachzuschauen, ob es sich eventuell um einen bekannten Bug handelt. Sollten Sie selbst einmal einen Bug finden, wäre es sehr nett, wenn Sie ihn hier eintragen und somit helfen, die Qualität des Zend Frameworks weiter zu verbessern. Eine weitere interessante Webadresse ist die folgende: http://framework.zend. com/changelog. Dabei handelt es sich um das Changelog zum Zend Framework. Hier finden Sie Informationen zu den Veränderungen innerhalb der einzelnen Versionen. Wenn Sie eine neue Version von Zend Framework installieren, so schauen Sie bitte dort nach, was sich geändert hat. Das Zend Framework ist noch jung und ein sehr »lebendiges« System. Es kann also durchaus einmal passieren, dass sich das Verhalten einer Methode oder eine API ändert. Als letzten Punkt möchte ich auf die Website http://www.zfibuch.de verweisen. Mit dieser Website möchte ich Sie als Leser ein wenig unterstützen. Sie finden dort Informationen zum Zend Framework und Ergänzungen sowie Korrekturen zu diesem Buch. 16
e-bol.net Einleitung Installation Das Zend Framework zu installieren, ist erfreulich einfach. Systemseitig gibt es eigentlich nur die Voraussetzung, dass mindestens PHP 5.1.4 vorhanden sein muss, wobei allerdings die Version 5.2.3 empfohlen wird. Ist diese Vorausset- zung erfüllt, müssen Sie einfach nur das Archiv, das Sie unter http://framework. zend.com finden, herunterladen und entpacken. In dem dabei entstehenden Ord- ner finden Sie ein Verzeichnis namens library und darin ein Verzeichnis namens Zend. In diesem Ordner liegt alles, was Sie benötigen. Sie können den Ordner library oder auch nur den Ordner Zend an eine beliebige Stelle auf Ihrem Server kopieren, falls Sie einen eigenen Server nutzen. Wenn möglich sollten Sie den Ordner nicht unterhalb des DocumentRoot-Verzeichnisses des Webservers able- gen. Der Webserver benötigt nur Leserechte auf das Verzeichnis; aber es gibt kei- nen Grund, warum das Verzeichnis von außen ansprechbar sein sollte. Sollte Ihnen nur gemieteter Webspace zur Verfügung stehen, ist das auch kein Problem. In dem Fall können Sie den Ordner auch mit im Webspace ablegen. Sie sollten den Ordner allerdings so sichern, dass er nicht von außen aufgerufen wer- den kann. Nachdem Sie den Ordner bereitgestellt haben, müssen Sie nur noch dafür sorgen, dass der Ordner oberhalb des Ordners Zend mit im Include-Path liegt. Haben Sie das Verzeichnis library kopiert müsste dieser Ordner also in den Include-Path. Bei dem Include-Path handelt es sich um ein oder mehrere Verzeichnisse, die PHP automatisch durchsucht, wenn Sie eine Datei mit requi re o.Ä. einbinden. Um den Ordner mit im Include-Path unterzubringen, gibt es verschiedene Mög- lichkeiten. Die beste Variante ist sicherlich, die Konfigurationsdatei php.ini ent- sprechend zu editieren. Nutzen Sie einen eigenen Server, finden Sie diese Datei meist im Ordner/etc. Sollten Sie für die Entwicklung auf Xampp, MAMP o.Ä. setzen, so müssen Sie in den entsprechenden Unterverzeichnissen nach der Datei suchen. In der Datei php.ini finden Sie eine Direktive namens include_path. Hier könnte beispielsweise include_path = "usr/share/php" stehen oder Ähnliches. Wenn Sie den Ordner Zend beispielsweise nach /usr/share kopiert haben, müssten Sie diesen Pfad hier ergänzen. Das heißt, die Direktive müsste danach (zumindest unter einem UNIX-Betriebssystem) so ausse- hen: include_path = /usr/share/php:/usr/share" 17
e-bol.net Einleitung Nutzen Sie Windows und haben Sie den Ordner unterhalb von d:\Webentwick- lung abgelegt, müssten Sie die ursprünglich vorhandene Angabe include_path = ".;c:\php\includes" so ergänzen: include_path = ".;c:\php\includes:d:\Webentwicklung" Was aber, wenn Sie keinen Zugriff auf die Datei php.ini haben? Die einfachste Variante ist dann sicher, dass Sie den Pfad direkt in der PHP-Datei festlegen, die ausgeführt wird. Würde sich der Ordner Zend unterhalb von /usr/share befin- den, könnten Sie die folgenden Zeilen am Anfang eines jeden PHP-Scripts ergän- zen: Spath = '/usr/share'; set_include_path(get_include_path() . PATH_SEPARATOR . Spath); Dabei würde es sich natürlich anbieten, die Zeilen in eine Datei auszulagern, die dann immer mit requi re_once eingebunden wird. Mit diesen Zeilen wird der aktuell gesetzte Pfad ausgelesen, dann um den zusätzlichen Pfad ergänzt und das Ganze wieder gespeichert. Eine weitere Möglichkeit ist, dass Sie den Pfad mit einer ,/itaccess-Datei setzen, die Sie in dem Verzeichnis ablegen, in dem auch die Applikation liegt. In dieser können Sie dann mit php_value include_path Zusr/1ocal/Iib/php;/usr/share" den Pfad setzen. Wichtig ist, dass Sie vorher mithilfe von get_incl ude_path() oder phpi nfo() die aktuellen Pfadeinstellungen auslesen und mit in die Zeile ein- fügen. Andernfalls würden die anderen Pfade verloren gehen. Danach können Sie sofort loslegen und das Zend Framework nutzen. Ist Ihnen das alles zu aufwändig, dann können Sie auch einfach zu Zend Core greifen. Dabei handelt es sich um einen Apache Webserver der ein fertig konfiguriertes PHP, inklusive Zend Framework, mitbringt. Allgemeines zu diesem Buch Es gibt ein paar Punkte, die Sie zu diesem Buch wissen sollten, um Dinge besser verstehen zu können. Der erste wichtige Punkt ist sicher, dass die meisten Bei- spiele nicht auf dem Model View Controller-Pattern basieren. Warum das so ist, fragen Sie? Nun, zum Ersten wäre es einfach zu komplex gewesen, die Pakete zu erläutern und dabei auch ständig die Funktionalitäten mit zu implementieren, 18
e-bol.net Einleitung die für ein MVC genutzt werden. Der zweite Punkt ist, dass man vielleicht nicht jede Anwendung mit einem MVC erstellen möchte. Wollen Sie beispielsweise nur Teile des Zend Frameworks in eine Applikation integrieren, ist es sicher sehr hilfreich, dass die Pakete standalone beschrieben sind. Das ist auch der Grund, warum Sie nur sehr selten Beispiele finden, in denen zwei Klassen gemischt wer- den. Das ist nur in wenigen Fällen gegeben, beispielsweise wenn ein Datenbank- zugriff mit Zend_Db erfolgt oder ein Datum mit Zend_Date konvertiert wird. Trotzdem finden Sie natürlich ein ausführliches Kapitel zum Aufbau und zur Nut- zung des MVC-Patterns. Ein weiterer Punkt, auf den ich hinweisen möchte, ist das Exception Handling. Hier mag man zuerst den Eindruck haben, dass ich geschlampt habe, aber dem ist nicht so. Ich habe bei den meisten Listings sehr bewusst darauf verzichtet, mit try-catch-Blöcken zu arbeiten. Der Grund dafür ist, dass dies bei der Vielzahl der Listings sehr viel Platz in Anspruch genommen hätte. Dennoch finden Sie an Stellen, an denen ich es für wichtig gehalten habe, auch entsprechende Blöcke. Wichtig ist in diesem Zusammenhang, dass Sie auf jeden Fall try-catch-Blöcke ergänzen sollten, sofern Sie Beispiele aus diesem Buch in Ihren Anwendungen nutzen möchten. In diesem Zusammenhang ist auch zu beachten, dass die meisten Beispiele darauf ausgelegt sind, Dinge zu erklären. Sie sind daher nicht immer elegant oder gar performant. Wahrscheinlich arbeiten die meisten von Ihnen nach wie vor im ISO-8859-1/-15- Zeichensatz. Daher sind die Listings alle auf diesen ISO-Zeichensatz ausgelegt. Trotzdem wird in vielen Zusammenhängen der UTF-8-Zeichensatz benötigt. Um das zu verdeutlichen, wird in diesen Listings immer ein UTF-8-Header mitge- schickt, und die Textdaten werden meist manuell mithilfe von utf8_encode() und utf8_decode() codiert bzw. decodiert. Falls Sie einen Fehler im Buch finden sollten, so würde ich mich freuen, wenn Sie mir eine kurze E-Mail an die E-Mail-Adresse zf-buch@netviser.de schicken wür- den. Allgemeines zur Nutzung Das Zend Framework ist nicht nur einfach zu installieren, sondern auch einfach zu nutzen. Außerdem ist es sehr klar strukturiert. 19
e-bol.net Einleitung Struktur des Zend Frameworks Das Zend Framework ist klar strukturiert. Am Namen der Klasse können Sie stets sofort erkennen, wo sich eine Datei befindet. Wollen Sie beispielsweise die Klasse Zend_Currency nutzen, so lautet die dazugehörige Klassen-Datei Zend/Currency.php. Jede Klasse besitzt eine eigene Datei, die Sie inkludieren bzw. laden können. Benötigt eine Klasse weitere Dateien, was fast immer der Fall ist, finden Sie diese in einem Unterverzeichnis, das dem Namen der Klasse ent- spricht. So benötigt beispielsweise Zend_Currency noch die Klasse Zend_ Currency_Exception. Diese ist somit in der Datei Zend/Currency/Exception.php deklariert. Grundsätzlich gilt also, dass Sie den Namen der Klasse nehmen, die Unterstriche durch Slashes ersetzen und die Endung .php anhängen, um den Namen der Datei zu erhalten. Ein wenig schade ist, dass die meisten Klassen nicht nach Themenbereichen gruppiert sind. Nur wenige Klassen, wie beispielsweise die Webservice-Klassen, sind nach Themenbereichen zusammengefasst. Diese Klassen, wie Zend_ Service_Amazon oder Zend_Service_Yahoo, finden Sie im Unterverzeichnis Zend/Service, wie Sie sicher schon vermutet haben. Erwähnt werden sollte noch, dass es einige Klassen gibt, bei denen in einem der Namensbestandteile ein Großbuchstabe vorkommt, wie beispielsweise bei Zend_ XmlRpc. Laden der Klassen-Dateien Sie können die Klassen-Dateien, die Sie benötigen, jederzeit mithilfe des Sprach- konstrukts requi re_once einbinden. Dies ist gleichzeitig das Vorgehen, das in diesem Buch an den meisten Stellen verwendet wird. Allerdings können Sie auch die Klasse Zend_Loader nutzen. Zend_Loader bietet eine Reihe von Möglichkeiten, um Dateien einzubinden. Um eine Klasse aus dem Zend Framework zu laden, müssen Sie lediglich den Namen der Klasse an die sta- tische Methode loadClassO übergeben. Diese lädt die Klassen-Datei und prüft dabei automatisch, ob die Klasse in der Datei enthalten ist. Sollte die Klasse schon vorhanden sein, so wird sie nicht doppelt eingebunden. Kommt es beim Laden zu einem Problem, so wirft die Methode eine Exception. Der ideale Weg, um eine Klasse aus dem Zend Framework zu laden, ist daher: requi re_once(’Zend/Loader.php'); Zend_Loader::loadClass('Zend_Gdata_Calendar'); 20
e-bol.net Einleitung Hiermit würde also automatisch die Datei Zend/Gdata/Calendar.php geladen. Wie bereits erwähnt, habe ich in dem Buch üblicherweise mit requi re_once() gearbeitet. Die Entscheidung für require_once() resultierte daraus, dass ich hoffe, auf diesem Weg die Transparenz der Beispiele zu erhöhen. Dennoch möchte ich Ihnen empfehlen, Zend_Loader:: 1 oadCl ass () zu nutzen, da Sie dann auch in der Lage sind, ein sauberes Exception Handling zu nutzen. Exception Handling Wie schon erwähnt, sind die Beispiele in diesem Buch nicht immer mit einem korrekten Exception Handling versehen. Genau genommen ist das sogar eher sel- ten der Fall. Das sollte Sie aber keinesfalls davon abhalten, dies besser zu machen. Grundsätzlich gilt, dass alle Fehler immer in einer Exception resultieren. An kei- ner Stelle geben die Methoden eine Fehlermeldung als Rückgabewert zurück. Hier und da lässt sich sicher darüber streiten, was ein Fehler ist und was nicht. Das heißt, einige Entwickler neigen auch dazu, bei kleineren Problemchen eine Exception auszulösen, wohingegen andere Entwickler das nur bei echten Fehlern tun. Daher sollten Sie immer damit rechnen, dass eine Exception auftreten kann. Das Zend Framework ist auch bei den Exceptions sehr strukturiert: Jede Klasse bringt ihre eigenen Exceptions mit. Somit können Sie sehr genau erkennen, an welcher Stelle ein Fehler entstanden ist. Der Name der Exception-Klasse ergibt sich (fast) immer aus dem Namen der Hauptklasse, die verwendet wird. Deren Namen wird einfach nur ein _Exception angehängt. Einige wenige Klassen ver- folgen hier allerdings einen anderen Ansatz, wie beispielsweise die Gdata-Klas- sen. Zuweilen wirft eine Klasse eine Exception, mit der man nicht gerechnet hat. Wenn Sie also beispielsweise einen RSS-Feed mit Zend_Feed auslesen, wird im Hintergrund die Klasse Zend_Http für den Verbindungsaufbau genutzt. Somit kann es passieren, dass Sie eine Exception dieser Klasse erhalten, obwohl Sie damit nicht gerechnet haben. Vor diesem Hintergrund empfiehlt es sich immer, dass Sie zusätzlich zu einem catch-Block für die Exceptions einer Klasse auf jeden Fall noch ein catch für die Klasse Exception nutzen. Da alle Exceptions Kind-Klassen der Klasse Exception sind, können Sie dann sicher sein, dass eine Exception, die nicht von einem vor- hergehenden catch »gefangen« wurde, spätestens dann beachtet wird. Bezogen auf die Verarbeitung eines RSS-Feeds könnte dies so aussehen: 21
e-bol.net Einleitung try { // Einlesen des RSS-Feeds } catch (Zend_Http_Exception $http_exception) { // Fehler beim Verbindungsaufbau verarbeiten } catch (Zend_Feed_Exception $feed_exception) { // Fehler bei der Verarbeitung der Daten } catch (Exception $al1gemeine_exception) { // Hier landen alle Exceptions mit dem man // nicht gerechnet hatte } Man kann an dieser Stelle sicherlich darüber diskutieren ob es Sinn macht Aus- nahmen zu fangen, die man nicht behandeln kann. Wollen Sie beispielsweise auf einen anderen Server zugreifen und dieser ist nicht verfügbar, dann können Sie daran meist auch nichts ändern. Meine persönliche Meinung ist aber, dass man eine Ausnahme immer behandeln sollte. Eine nett formulierte Fehlermeldung macht die meisten Benutzer glücklicher als eine »Unhandled Exception« Mel- dung von PHP. Performance Es liegt leider in der Natur der Sache, dass die meisten Frameworks eine Anwen- dung langsamer machen. Jede Software-Schicht, jede Abstraktion führt dazu, dass mehr Klassen und Objekte Verwendung finden. Soll Code robust sein, so müssen viele Abfragen für Eventualitäten enthalten sein. All das kostet Perfor- mance. Auch das Zend Framework stellt da keine Ausnahme dar. Um solche Ein- flüsse zu kompensieren empfiehlt es sich einen Opcode-Cache auf dem Produk- tiv-Server zu installieren. Dabei handelt es sich um einen Cache, der fertig übersetzten PHP-Code Zwischenspeichern kann. Somit können Sie den größten Teil der Geschwindigkeitsverluste die durch die Analyse und Interpretation des Codes entstehen kompensieren. In diesem Bereich gibt es eine ganze Menge empfehlenswerter Lösungen wie eAccellerator, APC oder Zend Platform. 22
e-bol.net Einleitung Sonstige Besonderheiten Sollten Sie noch nicht sicher im Umgang mit Objekten und Klassen sein, wäre jetzt ein guter Zeitpunkt, sich in die Thematik einzuarbeiten. Das Zend Frame- work nutzt durchweg modernste Entwicklungsansätze, sodass Interfaces, ab- strakte Klassen und Ähnliches keine Fremdworte für Sie sein sollten. Zwar haben Sie mit diesen Dingen nicht unbedingt immer direkt zu tun, aber es kann nicht schaden, die Ideen dahinter verstanden zu haben. Im Zusammenhang mit der Objektorientierung soll auch erwähnt werden, dass das Zend Framework an den meisten Stellen auf ein »Fluent Interface« setzt. Bei diesem »fließenden« Interface liefern Methoden, die keinen anderen Wert zurückgeben müssen, immer dasjenige Objekt zurück, aus dem heraus sie aufge- rufen wurden. Dies hat zur Folge, dass Sie mehrere Methoden direkt hinterein- ander aufrufen können. War Ihnen das zu abstrakt? Betrachten Sie hierzu die fol- gende Klasse: dass Rechner { private $_divident; private $_divisor; public function setzeWerte ($1, $r) { if ($r == 0) throw new Exception("Division durch Null"); 1 $this->_divident = $1; $this->_divisor = $r; return $this; public function dividieret) { return ($this->_divident / $this->_divisor); } Die Methode setzeWertet) muss keinen Wert zurückgeben, da hier nur Parame- ter für die Berechnung gesetzt werden. In einem konventionellen, prozeduralen Ansatz würde die Methode sicher true zurückgeben, falls die Zuweisung erfolg- reich war, und fal se, wenn es zu einem Problem kam. Da das Zend Framework aber mit Exceptions arbeitet, ist das nicht nötig. Bei einem Problem wird eine 23
e-bol.net Einleitung Exception geworfen und andernfalls $thi s zurückgegeben. Das hat zur Folge, dass Sie die Klasse wie folgt nutzen können: Srechner = new Rechner!): echo $ rechner- >setzeWerted, 3 )->di vidiere!); In vielen Fällen können Sie so eine recht stattliche Anzahl von Methoden mitein- ander verknüpfen. Das kann die Lesbarkeit verbessern, kann aber auch schnell verwirren. Überlegen Sie also gut, wie viele Methodenaufrufe Sie miteinander verknüpfen wollen. Ein weiterer Punkt, auf den ich hinweisen möchte, ist die Standard PHP Library (SPL). Sie ist ein PHP-Feature, das mit PHP 5 eingeführt wurde, und meiner Ansicht nach leider viel zu wenig Beachtung findet. Die SPL definiert eine ganze Reihe von Interfaces, welche für eigene Klassen genutzt werden können. In vie- len Klassen des Zend Frameworks finden Sie eine Implementation des Interfaces Iterator. Eine Klasse, die dieses Interface implementiert, kann beispielsweise direkt in einer foreach-Schleife genutzt werden. Enthält ein Objekt zum Beispiel verschiedene andere Objekte, könnte die Schleife bei jeder Iteration eines dieser Objekte auslesen. Sollten Sie nicht mit der SPL vertraut sein, so würde ich Ihnen empfehlen, dass Sie sich ein wenig mit ihr befassen. Eine Einleitung finden Sie in der PHP-Doku- mentation unter http://www.php.net/spl bzw. unter http://www.php.net/~helly/ php/ext/spl. Insbesondere das Interface Iterator sollten Sie sich dort anschauen. Hilfreich ist auch, wenn Sie verstehen, wozu die Interfaces ArrayAccess und Recursi velterator sowie die Klasse Recursi velteratorlterator dienen.
e-bol.net No one has ever drowned in sweat. - Lou Holtz, Football Coach 1 Der Model View Controller Das Kernstück des Zend Frameworks sind die Klassen zur Umsetzung des MVC- Entwurfsmusters. Die Abkürzung MVC steht für Model View Controller und beschreibt, wie eine Anwendung strukturiert werden kann. Das Zend Frame- work bringt hierbei eine große Menge Funktionalitäten mit, die Ihnen die Erstel- lung einer Applikation auf Basis eines MVC ermöglichen. Zur Umsetzung eines MVC werden mehrere Klassen miteinander kombiniert, sodass in diesem Kapitel - anders als in anderen Kapiteln - mehrere Klassen gleichzeitig genutzt und erläutert werden. Sie werden feststellen, dass das Thema recht komplex ist. Daher kann dieses Kapitel nicht alles erläutern. Dennoch denke ich, dass die wichtigen Inhalte hier abgebildet sind. Es kann aber auch nicht schaden, wenn Sie noch ein wenig in der Dokumentation stöbern. Sie finden dort noch viele hilfreiche Dinge wie bei- spielsweise die Helper. Bevor ich auf den praktischen Teil eingehe, möchte ich zunächst die Theorie erläutern. 1.1 Die Theorie des AAVC Wie schon erwähnt, ist die Idee des Model View Controllers, eine Anwendung in verschiedene Teile, nämlich das Model, den View und den Controller, aufzuglie- dern. Lassen Sie mich mit dem letzten Teil, nämlich dem Controller beginnen. Der Controller ist derjenige Teil der Applikation, der auf Benutzereingaben reagiert, die Eingaben verarbeitet und dafür sorgt, dass die korrekten Seiten dargestellt werden. Die eigentliche Darstellung der Daten wird vom View übernommen. Der View ist hierbei als eine abstrakte Komponente zu verstehen, da er sich aus mehreren 25
e-bol.net 1 | Der Model View Controller Teilen zusammensetzt. Neben der intern genutzten View-Klasse wird auch noch ein Template, also eine Design-Vorlage, genutzt, die für die Darstellung der Daten verwendet wird. Der Dritte im Bunde, das Model, ist für die Datenhaltung und -Verwaltung ver- antwortlich. Hierbei handelt es sich um eine Komponente, die dafür zuständig ist, die genutzten Daten unter anderem zu lesen und zu speichern. Gerne wird hierbei auch von einem Datenmodell gesprochen, wobei das vielleicht ein wenig verwirrend ist, da es sich nicht um ein Modell im Sinne eines Entity-Relation- ship-Modells, sondern um eine konkrete Klasse handelt. In den meisten Fällen finden Sie hier auch die Geschäftslogik. Die Geschäftslogik ist dafür zuständig Objekte und Vorgänge aus der realen Welt in die Programmie- rung umzusetzen. Das heißt, im Fall eines Shops könnte sich hier eine Klasse fin- den die dafür zuständig ist Bestellungen anzulegen und zu verwalten. Die Geschäftslogik im Model anzusiedeln macht Sinn, da sie sehr eng mit den Daten verknüpft ist. Beim Anlegen einer Bestellung müssen die Informationen bei- spielsweise in eine Datenbank geschrieben werden. Die Business-Logik stellt also einen weiteren Abstraktionslevel dar. Durch dieses Konzept haben Sie den Vor- teil, dass der komplette Zugriff auf die Daten gekapselt ist. Im Controller müssen Sie also nur noch die Eingaben des Benutzers entgegen nehmen und sie an die Methoden der Model-Klassen übergeben. Durch diese klare Struktur wird der Code deutlich wartbarer und übersichtlicher. Sollten Sie sich nun fragen, wie man Model und Controller trennscharf definiert, so ist das berechtigt. Die Idee hinter der Aufteilung ist, die Daten und Funktiona- litäten zu kapseln. Das heißt, das Model muss so strukturiert sein, dass alles, was die Speicherung und das Auslesen betrifft, hierin enthalten ist. Möchten Sie die Daten in einer Datenbank abspeichern, so muss des Escapen von Sonderzeichen ein Teil des Models sein. Auch Methoden zum Berechnen von Preisen, der Ver- waltung von Lagerbeständen und Ähnliches sind hier anzusiedeln, da diese Funk- tionalitäten zur Geschäftslogik gehören. Die Aufbereitung der Daten für die Darstellung hingegen ist Teil des Controllers. 1.2 Die Praxis des AAVC Eine Applikation auf Basis eines MVC umzusetzen, erscheint auf den ersten Blick ein wenig komplex, aber keine Angst, so schlimm ist's nicht. Eine MVC-Applikation besitzt immer einen zentralen Einstiegspunkt. Es gibt also exakt eine Datei, die bei jedem Aufruf angesprochen wird. Hierbei handelt es sich 26
e-bol.net Die Praxis des MVC | 1.2 um den Front-Controller, der auch Bootstrap-File genannt wird. Bei genauer Betrachtung ist die Bezeichnung Front Controller vielleicht ein wenig verwirrend. Die Datei bietet die Funktionalität eines Front-Controllers, nutzt aber gleichzeitig auch ein Objekt der Klasse Zend_Cont rol l er_Front. Da Datei und Objekt sehr eng miteinander verknüpft sind, kann man mit dieser kleinen Ungenauigkeit leben. Die Datei ist allerdings nicht so komplex, wie Sie vielleicht gerade befürchten. Der Front Controller ist im Endeffekt nur dafür zuständig, die Anfrage entgegenzuneh- men und sie an einen »Action-Controller« weiterzugeben, in dem die eigentliche Logik enthalten ist. Sie können sich den Front Controller also wie einen Manager vorstellen, der dafür zuständig ist, die Applikation zu verwalten. Rein technisch ist das zwar nicht ganz korrekt, aber an dieser Stelle soll das so erst einmal reichen. Wie Sie vielleicht schon ahnen, wird im Hintergrund eine Menge »Magie« genutzt, welche die einzelnen Komponenten verknüpft. Das heißt, das Frame- work ist in der Lage, automatisch die notwendigen und korrekten Komponenten einzubinden. Dazu muss Ihre Applikation sich an eine vordefinierte Verzeich- nisstruktur halten. Die Dokumentation des Zend Frameworks schlägt dafür diese Verzeichnisstruktur vor: application/ controllers/ models/ views/ scripts/ heipers/ fl1ters/ html / Die Ordner html und application befinden sich in diesem Beispiel auf einer Ebene, wobei nicht erforderlich ist. Es könnte sich beispielsweise auch um die Ordner /var/application und /var/www/html handeln. Das Directory html ist das eigentliche Document-Root-Verzeichnis des Servers. Hier würden normalerweise die HTML- bzw. PHP-Dateien abgelegt. Der Ordner muss übrigens nicht unbe- dingt html heißen. Er könnte genauso gut htdocs oder public_html heißen. Der Ordner application sollte nicht direkt unterhalb des Document-Root-Ver- zeichnisses des Servers liegen. Somit ist sichergestellt, dass niemand unberechtig- ten Zugriff auf eine der Dateien hat. Der Name application ist übrigens frei gewählt. Sie könnten hier auch einen beliebigen anderen Namen nutzen. Die Namen der Verzeichnisse, die darunter genutzt werden, sollten Sie allerdings übernehmen. Im Unterverzeichnis Controllers werden die Controller-Klassen abgelegt, im Ordner views/scripts die Templates für die Darstellung und in models die Dateien, die für den Datenzugriff erforderlich sind. 27
e-bol.net 1 | Der Model View Controller Im Ordner html befindet sich unter Umständen nur eine einzige Datei, der Front Controller. Abhängig von der Applikation wären hier natürlich noch Grafiken, CSS-, JavaScript-Dateien oder Ähnliches zu finden. Bevor ich auf die Controller eingehe, stellt sich noch die Frage, wie Sie sicherstel- len können, dass die Anfragen auch wirklich alle beim Front Controller landen. Um das zu gewährleisten, sollten Sie auf die Rewrite-Engine Ihres Webservers zurückgreifen. Alle aktuellen Webserver unterstützen die Möglichkeit des URL- Rewritings, wobei unter Umständen, wie beim IIS, zusätzliche Module notwen- dig sind. Beim URL-Rewriting wird die URL, die ein Browser aufgerufen hat, ein- fach so umgeschrieben, dass ein bestimmtes Ziel angesprochen wird. In diesem Fall würde also jeder eingehende Aufruf auf die Datei index.php »umgebogen«. Genau genommen handelt es sich nicht um jeden Aufruf. Grafiken, statische HTML-Seiten, CSS-Dateien und alles andere, was nicht unmittelbar mit der Appli- kation zu tun hat, kann direkt ausgeliefert werden. Leider sind die Webserver alle unterschiedlich zu konfigurieren. Daher werde ich hier nur auf den Apache-Webserver eingehen. Für andere Webserver ziehen Sie bitte das Manual Ihres Webservers zu Rate.1 Im Fall von Apache können Sie die Rewrite-Rule entweder in der entsprechenden Konfigurationsdatei Ihres Servers angeben oder Sie nutzen eine .htaccess-Datei, was wahrscheinlich einfacher und flexibler ist. Der Inhalt der Datei könnte bei- spielsweise so aussehen: RewriteEngine on RewriteRule I\.(js|ico|gif|jpg|png|ess)$ index.php Diese beiden Zeilen müssten unter dem Namen .htaccess im Document-Root-Ver- zeichnis des Servers gespeichert werden. Hiermit werden zwei Dinge definiert. In der ersten Zeile wird die Rewrite-Engine des Servers eingeschaltet. Üblicher- weise ist das kein Problem. Sollte das dafür notwendige Modul mod_rewrite nicht verfügbar sein, müssten Sie es nachinstallieren oder sich mit Ihrem Admi- nistrator bzw. Provider in Verbindung setzen. In der zweiten Zeile finden Sie die eigentliche Regel, die für das Rewriting zustän- dig ist. Im Endeffekt handelt es sich bei dem ersten Teil (! \. (js | i co | gi f | jpg|png|css|css|htm)$) um einen regulären Ausdruck, der auf alle URLs zutrifft, die nicht auf eine der enthaltenen Endungen enden. Sollten Sie noch weitere Dateiformate nutzen, die nicht in der Liste enthalten sind, beispielsweise 1 Die Dokumentation zum Zend Framwork beinhaltet auch einige Informationen zu anderen Webservern: http://framework.zend.com/manual/en/zend.controller.router.html 28
e-bol.net Die Praxis des MVC | 1.2 RSS oder WSDL, so müssten Sie diese noch ergänzen. Der zweite Teil der Regel (i ndex. p hp) ist das Ziel, auf das umgeleitet wird, also die Datei index.php. Sollten Sie das MVC-Projekt nicht im Root-Verzeichnis des Servers entwickeln, kann es passieren, dass Sie noch die Direktive RewriteBase ergänzen müssen. Hinter RewriteBase geben Sie dann bitte den absoluten Pfad des Verzeichnisses an, das Sie nutzen. 1.2.1 Der Front Controller Somit ist nun sichergestellt, dass jede Anfrage beim Front Controller landet. Der Front Controller kann im Prinzip sehr einfach aufgebaut sein. Diese beiden Zei- len würden eigentlich schon reichen: requi re_once 'Zend/Controller/Front.php’; Zend_Controller_Front::run(’/var/www/applicati on/control l ers'); Das wäre eigentlich schon alles, was Sie benötigen. Der statischen Methode run() wird in diesem Fall einfach nur der Pfad zum Verzeichnis des Controllers übergeben. Der Rest ist Magie! Allerdings würde ich Ihnen eine etwas umfangreichere Variante vorschlagen, da Sie dann über bessere Möglichkeiten zur Konfiguration verfügen. Diese Variante könnte so aussehen: requi re_once 'Zend/Controller/Front.php’; // Controller-Instanz auslesen $fc = Zend_Controller_Front::getlnstance(): // Verzeichns setzen $fc->setControllerDirectoryi'/var/www/appl/controllers’): // Nutzung von Views unterdrücken $fc->setParam('noViewRenderer’, true): // So einstellen, dass Ausnahmen geworfen werden $fc->throwExceptions(true); // Error-Handler ausschalten $fc->setParam('noErrorHandler’, true): $fc->di spatch(); Listing 1.1 Ein einfacher Front Controller Diese Einstellungen sehen auf den ersten Blick vielleicht ein wenig komplex aus, dafür bieten sie aber ein hohes Maß an Flexibilität. Durch den Aufruf der Methode Zend_Control l er_Front: :getlnstance(), die übrigens keine Parameter akzeptiert, wird die aktuelle Instanz des Front Control- 29
e-bol.net 1 | Der Model View Controller lers ausgelesen, der als Singleton-Pattern implementiert ist. Damit stellt das Fra- mework sicher, dass immer nur ein Objekt des Front Controllers abgeleitet wird. Da der Front Controller sich darum kümmern soll, dass der korrekte Action Con- troller eingebunden wird, muss er wissen, wo die Action Controller zu finden sind. Sie teilen ihm dies mittels der Methode setControl l erDi rectory() mit. Um die ersten Beispiele möglichst einfach zu halten, wird die Nutzung eines Views mithilfe von $fc->setParam( ’noViewRenderer', true) unterdrückt. Die Methode setParam() kann natürlich noch einiges mehr für Sie tun, wie Sie spä- ter noch sehen werden. Damit wäre die grundsätzliche Konfiguration abgeschlossen. Gerade für die Ent- wicklung ist es sinnvoll, noch einige zusätzliche Einstellungen festzulegen. Da wäre zunächst der Umgang mit Exceptions. Die Model View Controller-Im- plementation des Zend Frameworks ist darauf ausgelegt, möglichst robust zu sein. Das heißt, dass sie eine Exception erst einmal fängt, um ein möglichst gutes Error-Handling zu ermöglichen. Allerdings führt das bei der Entwicklung und dem Debugging dazu, dass Fehler unter Umständen schwer zu finden sind. Daher ist es hilfreich, dem Controller mitzuteilen, dass Exceptions tatsächlich geworfen werden sollen, was mit $fc->throwExceptions(true) geschieht. In der nächsten Zeile wird mithilfe von $fc->setParam('noErrorHandler’, true) sichergestellt, dass das interne Error-Handling unterdrückt wird. Die eigentliche Verarbeitung des Aufrufs übernimmt die Methode di spatch(). Etwas verallge- meinert formuliert bindet sie nun den entsprechenden Action Controller ein. 1.2.2 Der Action Controller Nachdem Sie nun einen Überblick über den Front Controller erhalten haben, stellt sich die Frage, wie der Action Controller aufgebaut sein muss. Im nächsten Listing sehen Sie eine Minimalversion eines Action Controllers: requi re_once 'Zend/Controller/Action.php'; dass Indexcontroller extends Zend_Control ler_Action ( public function indexAction() { echo "Hallo, ich bin die Index-Action aus dem Index-Controller"; } Listing 1.2 Der erste Action Controller 30
e-bol.net Die Praxis des MVC 1.2 Die Namen der Klasse und der Methode habe ich mir übrigens nicht einfach nur ausgedacht. Warum beide mit Index bzw. Index anfangen, erfahren Sie gleich noch. An dieser Stelle möchte ich lediglich darauf hinweisen, dass der Name einer Controller-Klasse immer mit einem Großbuchstaben anfangen und auf Controller enden muss. Des Weiteren muss ein Action Controller immer die Klasse Zend_Control 1 er_Action erweitern. Die Namen der Methoden, die von außen ansprechbar sein sollen, müssen mit einem Kleinbuchstaben beginnen und auf Action enden. Dieser Controller muss nun in einer Datei namens IndexController.php im Con- troller-Verzeichnis abgelegt werden, das dem Front Controller mit der Methode setControl 1 erDi rectory() mitgeteilt wurde. Der Front Controller bindet auto- matisch die Datei ein, leitet ein Objekt der Klasse ab und ruft die Methode 1ndex- Actionl) auf. Das Ergebnis sehen Sie in Abbildung 1.1. Abbildung 1.1 Die erste Ausgabe der Anwendung Wie Sie sehen, wird der Index-Controller aufgerufen, ohne dass ich dem Front Controller mitgeteilt habe, dass er dies tun soll. Aber warum ist das so? Solange Sie dem Front Controller nichts anderes mitteilen, ruft er automatisch die Index- Action im Index-Controller auf. Auch die dazugehörige Datei wird dementspre- chend automatisch eingebunden, sodass Sie sich um nichts kümmern müssen. Diese gesamte Funktionalität kann nur dann gewährleistet werden, wenn alle Komponenten korrekt benannt sind. Natürlich werden Sie nicht nur den Index-Controller aufrufen wollen. Dann müssten Sie ja die komplette Logik in den Index-Controller einbauen, was ihn ziemlich komplex werden ließe. 31
e-bol.net 1 | Der Model View Controller Daher hier ein zweiter Controller - nennen wir ihn FooControl l er: requi re_once 'Zend/Controller/Action.php'; dass FooControl ler extends Zend_Control ler_Action { public function indexAction() { echo "Hier ist die indexAction aus dem FooController"; } Dieser Controller muss in einer Datei mit dem Namen FooController.php gespei- chert werden. Um ihn anzusprechen, können Sie den Front Controller auf zwei Wegen aufrufen: http://127.O.O.l/Foo http://! 27.0.0.1/index.php/Foo Die Angabe 127.0.0.1 müssten Sie eventuell durch den Namen oder die IP- Adresse des Rechners ersetzen, den Sie ansprechen wollen. In allen Beispielen dieses Kapitels werde ich immer die 127.0.0.1 nutzen. Über beide URLs wird nun dem Action Controller mitgeteilt, dass der Controller FooController genutzt werden soll. Sollte die erste Variante bei Ihnen in einem »404-Fehler« resultieren, dann liegt das daran, dass das URL-Rewriting bei Ihnen nicht funktioniert. Der zweite Aufruf müsste aber funktionieren. Sollten beide Varianten eine Exception zur Folge haben, prüfen Sie bitte, ob die Pfade alle kor- rekt gesetzt und die Klasse und die Methode korrekt benannt sind. Falls Sie noch eine zusätzliche Action ergänzen, die beispielsweise ba rActi on hei- ßen könnte, haben Sie schon deutlich mehr Möglichkeiten: public function barAction() { echo "Hier ist die barAction aus dem FooController"; 1 Diese neue Action können Sie folgendermaßen ansprechen: h ttp://127.0.0.1 /Foo/bar http://127.0.0.1/index.php/Foo/bar Wobei Sie natürlich auch FOO.foo oder f00 schreiben könnten. Das Zend Fra- mework normalisiert die Schreibweise so, dass der Aufruf immer beim korrekten Controller landet. 32
e-bol.net Die Praxis des MVC | 1.2 Wie Sie sich nun sicher schon denken, ist der erste Parameter, der nach dem Slash übergeben wird, der Name des Controllers, der genutzt werden soll, und der zweite ist der Name der Action, die ausgeführt werden soll. Geben Sie weder Controller nach Action an, wird beides implizit durch Index ersetzt, sodass die Anfrage beim Indexcontroller landet und an die indexAction weitergereicht wird. Geben Sie nur einen Controller an, wird dieser Controller genutzt und i ndexActi on in diesem Controller angesprochen. Daher sollten Sie aufjeden Fall immer eine Methode namens i ndexActi on in jedem Controller haben. Natürlich können Sie den IndexControl 1er auch explizit ansprechen, indem Sie den Front Controller so aufrufen: http://! 2 7.0.0.1/index/index http'.//l 27.0.0.1/index.php/index/index Gar nicht so schwer, oder? Bei sehr großen Anwendungen können Sie die Con- troller auch noch in Module untergliedern, um Ihre Anwendungen noch weiter zu strukturieren. Sie können also mehrere Verzeichnisse für die Controller nut- zen. In dem Fall übergeben Sie über die URL als ersten Wert den Namen des Moduls, gefolgt von dem Namen des Controllers und der Action. Auch in diesem Fall set- zen Sie die Namen der Verzeichnisse mit der Methode setControl1erDirec- toryO. Hier bekommt sie allerdings ein Array übergeben und nicht nur den Namen eines Verzeichnisses: $fc->setControl1erDi rectoryl array( ’default' => '/var/www/appl/control1ers', ’cms' => '/var/www/cms/control1ers’ , ’admin' => '/var/www/admin/control1ers' )); Hierbei ist wichtig, dass es einen Eintrag namens defaul t gibt. Das Verzeichnis, das damit angegeben wird, ist das Default-Verzeichnis, das beispielsweise ange- sprochen wird, wenn Sie das Modul nicht explizit angeben. Bei den Verzeichnis- sen ist zu beachten, dass die Controller immer in einem Verzeichnis namens Con- trollers liegen sollten. Anders formuliert: Sie können die Verzeichnisebenen dazwischen (appl, cms, admi n) nutzen, um Ihre Anwendung zu strukturieren. Das Interessante dabei ist, dass der Front Controller erkennt, ob Sie den Modul- namen angegeben haben oder nicht. Die folgenden URLs würden alle den Index- Controller im Default-Modul ansprechen: 33
e-bol.net 1 | Der Model View Controller http://12 7.0.0.1 /default/index/index http://l 2 7.0.0.1/index http://127.0.0A Wollen Sie ein anderes Modul ansprechen, müssen Sie den Namen des Moduls explizit angeben: http://12 7.0.0.1/cms/ http://l 2 7.0.0.1/cms/index http://12 7.0.0.1/cms/index/index Das Ganze funktioniert so, dass der Front Controller die übergebene URL analy- siert und schaut, ob als erster Parameter der Name eines Moduls übergeben wurde. Ist das nicht der Fall, wird automatisch das Default-Modul genutzt. Diese Vorgehensweise hat natürlich zur Folge, dass Sie im Default-Modul keinen Con- troller nutzen sollten, der den selben Namen hat wie ein Modul. Andernfalls könnte der Aufruf zweideutig sein, oder Sie müssen sicherstellen, dass dieser Controller nur mit vorangestelltem Modulnamen aufgerufen wird. In allen Beispielen werde ich allerdings auf die Nutzung von Modulen verzichten und immer nur mit einem Controller-Verzeichnis arbeiten. 1.2.3 Einbinden eines Views Nachdem Sie nun wissen, wie der Controller eingebunden wird, stellt sich die interessante Frage, wie die View-Komponente ins Spiel kommt. Dazu müssen Sie im Front Controller zunächst die Nutzung der Views aktivieren. Ändern Sie dazu die Zeile, in der der Parameter noViewRenderer gesetzt wird, so ab, dass ihr der Wert fal se zugewiesen wird: $fc->setParam('noViewRenderer' , false): Versuchen Sie jetzt einen der Controller aufzurufen, resultiert das in einer Zend_ View_Exception, die Ihnen mitteilt, dass das entsprechende Template nicht gefunden wurde. Das bedeutet, dass Sie nun ein Template anlegen müssen. Bei den Templates han- delt es sich um ganz normale HTML-Seiten, die auch PHP enthalten dürfen. Bei genauerer Betrachtung muss hier sogar ein wenig PHP enthalten sein. Ein solches Template wird direkt vom Front Controller, genau genommen vom Dispatcher, der im Hintergrund die Arbeit erledigt, mit eingebunden. Das Template, auf dem die folgenden Beispiele aufbauen, sieht so aus: CDOCTYPE html PUBLIC " -//W3C//DTD HTML 4.01 Transitional//EN” "http://www.w3.org/TR/html4/loose.dtd"> 34
e-bol.net Die Praxis des MVC | 1.2 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=IS0-8859-l"> <title>Mein erstes Tempiate</title> </head> <body> <?php ?> </body> </html> Listing 1.3 Grundlegendes Template Wie Sie sehen, ist es wirklich eine ganz normale HTML-Datei. Der PHP-Abschnitt in der Mitte wird gleich noch mit Inhalt gefüllt. Abgespeichert werden muss das Template im Ordner views/scripts, der sich unterhalb des Verzeichnisses appl befindet. Wie und wo genau Sie die Datei abspeichern, hängt davon ab, für wel- chen Controller und welche Action sie gedacht ist. Standardmäßig geht das Zend Framework davon aus, dass Sie für jeden Controller ein eigenes Unterverzeichnis anlegen, und dass die Datei den Namen der Action bekommt, für die das Temp- late zuständig ist. Wenn also das Template für den Indexcontroller und die indexAction gedacht ist, so würde es mit dem Namen index.phtml im Ordner index unterhalb von views/scripts gespeichert. Der komplette Pfad würde also /var/www/appl/views/scripts/index/index.phtml lauten. Ein Template für die barAction aus dem FooController würde also unter /var/www/appl/views/ scripts/foo/bar.phtml gespeichert. Aber keine Angst, Sie können ein Template auch für mehrere Actions nutzen, sodass Sie nicht in einer Flut von Templates ertrinken. Wie das funktioniert, erfahren Sie später. Zwar können Sie die Seite jetzt schon aufrufen, aber es gibt noch ein kleines Pro- blem. Das fällt allerdings erst dann auf, wenn Sie sich den Quelltext anschauen, der im Browser ankommt. Dieser lautet folgendermaßen: CDOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> Cmeta http-equiv="Content-Type” content="text/html; charset=IS0-8859-l"> <title>Mein erstes Tempiate</title> </head> <body> </body> 35
e-bol.net 1 | Der Model View Controller </html> Hallo, ich bin die Index-Action aus dem Index-Controller Grundsätzlich sieht das ja nicht schlecht aus, nur hätte der Text nicht hinter dem schließenden </html>-Tag landen sollen, sondern im Body der Seite. Das Pro- blem ist, dass das System natürlich nicht wissen kann, wo die Ausgabe landen soll. Eine Ausgabe mit echo oder print ist daher keine gute Idee, falls Sie die Daten in einem Template ausgeben wollen. Zu Beginn des Kapitels hatte ich angedeutet, dass der View im MVC aus mehre- ren Komponenten besteht. Nun kennen Sie das Template, aber im Hintergrund gibt es auch noch ein Objekt der Klasse Zend_Vi ew, das für die Ausgabe zuständig ist. Mithilfe dieses Objekts können die Action Controller auch mit dem Template »kommunizieren«, ihm also Daten übergeben. Und das geht so: Innerhalb der Action, die ausgeführt wird, lesen Sie eine Refe- renz auf das Zend_Vi ew-Objekt aus. In diesem können Sie nun Eigenschaften mit Werten belegen. Dieses Objekt steht dann auch im Template zur Verfügung und kann über $this angesprochen werden. Die Methode indexAction im Index- Controller müssten Sie folgendermaßen umstellen: public function indexAction!) { $view = $this->initView(); $view->ausgabe = "Hallo, ich bin die Index-Action aus dem Index-Controller"; } Listing 1.4 Zuweisen eines Textes Der Action Controller legt das View-Objekt in der Eigenschaft view ab. Die Methode i ni tVi ew() liest diese Eigenschaft aus und gibt Ihnen eine Referenz auf das View-Objekt zurück. Zwar könnten Sie auch direkt auf die Eigenschaft zugrei- fen, aber die Nutzung von i ni tVi ew() hat den Vorteil, dass die Eigenschaft kor- rekt initialisiert wird, sofern dort noch kein Objekt der Klasse Zend_Vi ew enthal- ten ist. Dem so ausgelesenen Objekt können Sie dann direkt die Werte zuweisen, wie Sie das in diesem Beispiel sehen. Der Name der Eigenschaft ist frei gewählt. Alterna- tiv können Sie auch eine andere Syntax nutzen, die für Außenstehende vielleicht besser zu verstehen ist. Und zwar haben Sie die Möglichkeit, einer Eigenschaft mithilfe der Methode assignt) einen Wert zuzuweisen. Bezogen auf dieses Bei- spiel würde das so aussehen: 36
e-bol.net Die Praxis des MVC | 1.2 $view = $this->initView(); $view->assign(’ausgabe', 'Hallo, ich bin die Index Action aus dem Index Controller'): Wählen Sie einfach die Variante, die Ihnen am besten gefällt. In beiden Fällen geschieht dasselbe. Wobei die Aussage nicht ganz richtig ist. Der Methode assi gn() können Sie auch ein assoziatives Array übergeben. In dem Fall werden die Schlüssel des Arrays als neue Eigenschaften genutzt, denen dann die dazuge- hörigen Werte zugewiesen werden. In beiden Fällen gilt allerdings, dass die Namen der Eigenschaften, die Sie verge- ben, nicht mit einem Unterstrich (_) beginnen dürfen. Versuchen Sie einen sol- chen Namen zu nutzen, resultiert das in einer Exception der Klasse Zend_Vi ew_ Exception. Der Hintergrund ist, dass die internen Eigenschaften alle mit einem Unterstrich beginnen und keinesfalls überschrieben werden dürfen. Im Template selbst könnten Sie den Inhalt der Eigenschaft einfach mit echo $this ->ausgabe; ausgeben. Allerdings sollten Sie natürlich immer sicherstellen, dass die Daten, die Sie ausgeben, nicht problematisch sein können. Das heißt, es muss sichergestellt sein, dass kein HTML-Code enthalten ist, der dafür sorgen könnte, dass die Darstellung leidet, oder dass der Browser, der die Daten anzeigt, mit JavaScript kompromittiert wird. Dafür ist in der Klasse Zend_View die Methode escape() deklariert. Die bessere Variante zur Ausgabe der Daten ist also diese: echo $this->escape($this->ausgabe); Die Methode escapel) ersetzt HTML-Sonderzeichen (< , >, &, ") durch Entitäten. Sollten die Daten schon escapet sein, weil sie beispielsweise aus Zend_Filter_ Input übernommen wurden, dürfen sie hier natürlich nicht noch einmal codiert werden. Damit haben Sie die erste kleine Implementation eines MVC umgesetzt. 1.2.4 Die Verarbeitungsschritte Nachdem Sie nun einen guten Überblick haben, wie ein MVC funktioniert, möchte ich einen kleinen Rückschritt machen und noch ein paar Worte dazu ver- lieren, was im Hintergrund passiert. Wie schon erwähnt, ist der Front Controller derjenige Teil, der sich um den gesamten Ablauf der Verarbeitung kümmert. Das heißt, er nimmt die eingehende Anfrage entgegen und sorgt dafür, sie an den Router weiterzugeben. Der Router analysiert die Anfrage und prüft, welcher Action Controller zu nutzen ist. Mit 37
e-bol.net 1 | Der Model View Controller dieser Information ausgestattet ist der Dispatcher dann der Nächste in der Reihe. Er bindet den Action Controller ein und ruft die entsprechende Methode auf. Danach wird die Antwort an den Client geschickt. Damit alle Beteiligten miteinander kommunizieren können, werden die Anfrage (der Request) und die Antwort (die Response) in eigenständigen Objekten abge- legt. Auf das Request-Objekt kann von allen Komponenten zugegriffen werden, wohingegen das Response-Objekt nur von einem Action Controller aus genutzt werden kann. Zugegebenermaßen ist das nur ein kurzer Abriss, wie die Verarbeitungsschritte aussehen. Ich meine aber, dass diese Information erst einmal ausreichend ist, um das meiste zu verstehen. Weitere Informationen dazu können Sie der Dokumen- tation unter der Adresse http://framework.zend.com/manual/en/zend.controller. basics.html entnehmen. 1.2.5 Übergabe von Werten Die meisten Webanwendungen geben nicht nur Daten aus, sondern benötigen auch Informationen vom Benutzer. Meist kann der Anwender Daten in ein For- mular eingeben, die dann von der Anwendung verarbeitet werden. Dabei stellt sich die Frage, wie Ihre Anwendung an diese Daten kommt. Die einfachste Möglichkeit ist sicher, wenn Sie in altbekannter Weise direkt auf $_GET, $_POST etc. zugreifen. Auch in Zeiten des Model View Controllers funkti- oniert das ohne Probleme. Allerdings sieht das Zend Framework an dieser Stelle auch eine elegantere Mög- lichkeit vor. Das Request-Objekt Alle Daten, die mit der clientseitigen Anfrage im Zusammenhang stehen, werden im Request-Objekt abgelegt. Dieses Request-Objekt ist im Action Controller bekannt und kann mit der Methode getRequestO ausgelesen werden. Das Objekt der Klasse Zend_Control l er_Request_Http stellt verschiedene Möglich- keiten zur Verfügung, wie Sie auf die Daten zugreifen können, die mit dem Request übertragen wurden. Die Methoden, deren Name immer mit get beginnt, haben alle ein einheitliches Call-Interface. Sie können also alle auf dieselbe Art und Weise aufgerufen werden. 38
e-bol.net Die Praxis des MVC | 1.2 Wollen Sie einen Wert auslesen, der mit der Methode POST aus einem Formular übergeben wurde, können Sie die Methode getPostO nutzen. Sie greift im Hintergrund auf das superglobale Array $_POST zu und bekommt den Namen des Schlüssels übergeben, den Sie auslesen wollen. Wollen Sie also auf $_POST [' name'] zugreifen, so würden Sie im Action Controller die folgenden Zeilen benötigen: Srequest = $this->getRequest(); $name = $request->getPost(’name'): Die Nutzung der Methode getPost() hat zwei Vorteile. Erstens nutzt getPostt) - und alle anderen get-Methoden tun das auch - intern ein i sset(), sodass Sie sich keine Gedanken machen müssen, ob ein bestimmter Wert vorhanden ist. Ist ein Wert in dem entsprechenden superglobalen Array (in diesem Fall also $_POST) nicht vorhanden, gibt die Methode NULL zurück. Der zweite Vorteil ist, dass Sie der Methode einen Default-Wert übergeben kön- nen, der zurückgegeben wird, falls der gesuchte Wert nicht im Array enthalten ist. Die Zeile $name = $request->getPost(’name', 'kein Name übergeben'); speichert in $name also entweder den Inhalt von $_POST[' name' ] oder, falls der Schlüssel in dem Array nicht vorhanden ist, »kein Name übergeben«. In Tabelle 1.1 finden Sie die verschiedenen Methoden, die für den Zugriff auf die superglobalen Arrays deklariert sind. Methode Superglobales Array getPost() $_POST getQuery() $_GET getServer() $_SERVER getCookie() $_COOKIE getEnv() $_ENV Tabelle 1.1 Methoden zum Auslesen von superglobalen Arrays Für $_SESSION ist keine Zugriffsmethode deklariert, da zur Verwaltung von Ses- sions die Klasse Zend_Session genutzt werden sollte. Wenn Sie eine dieser Methoden aufrufen, ohne ihr einen Parameter zu überge- ben, erhalten Sie übrigens das komplette superglobale Array zurück. Eine besondere Rolle kommt noch der Methode getParam() zu. Sie liest einen Parameter aus, welcher dem Action Controller übergeben wurde. Wenn Sie nun fragen, was ein Parameter ist, dann kann ich nur antworten: Das kommt darauf 39
e-bol.net 1 | Der Model View Controller an. Es gibt drei Möglichkeiten, was die Methode Ihnen zurückgeben kann. Die erste Variante ist ein Parameter, der über die URL übergeben wurde. Wenn Sie Ihre Applikation also folgendermaßen http:/ /127.0.0.1/i ndex/index/ort/Bi elefeld/plz/33602 ansprechen, werden hier die Parameter ort und pl z übergeben. Dabei bekommt ort bekommt den Wert »Bielefeld« zugewiesen und plz erhält »33602«. Das heißt, $request->getParam( 'ort'): liefert »Bielefeld« zurück. Der Vollständig- keit halber sollte ich an dieser Stelle erwähnen, dass Sie auch die Parameter modale, Controller und action abfragen können. Der erste enthält in diesem Beispiel default und die beiden anderen Index. Sie können also Werte über die URL übergeben, wobei diese dann immer paar- weise zusammengefasst werden. Der erste Wert nach dem Namen der Action ist der Name des ersten Parameters, worauf der dazugehörige Wert folgt. Dann kommt der Name des zweiten Parameters, der dazugehörige Wert usw. Nun hatte ich eingangs erwähnt, dass es nicht ganz einfach zu beantworten ist, was ein Parameter ist. Das liegt daran, dass die Methode getParam() erst nach einem Wert schaut, der über die URL übergeben wurde, anschließend $_GET prüft, ob ein entsprechender Schlüssel vorhanden ist, und dann noch in $_POST danach sucht. Erst wenn in allen Fällen keine Daten zu finden sind, gibt die Methode NULL bzw. den Default-Wert, den Sie als zweiten Parameter übergeben haben, zurück. Die Methode liefert dabei jeweils den ersten Treffer, der gefun- den wird. Der Zugriff über get Par am () empfiehlt sich meiner Ansicht nach nur dann, wenn Sie auch wirklich einen Wert über die URL übergeben. Sie sollten die Methode nicht nutzen, falls Sie Werte auslesen wollen, die mit GET oder POST übergeben wurden. Ebenso sollten Sie auch eine Namensgleichheit zwischen URL-Parame- tern und Namen von Formularfeldern vermeiden, da die übergebenen Werte sonst eventuell nicht eindeutig sind. Wichtig ist, dass Sie immer beachten, dass in der URL die Angabe von Controller und Action nicht fehlen darf. Parameter, die Sie über die URL übergeben, können beispielsweise sehr hilfreich sein, wenn es sich um Daten handelt, die Bestandteil eines Links sein sollen. Das könnte zum Beispiel die Übergabe einer ID oder der Name eines Produkts sein. Dies kann den angenehmen Nebeneffekt haben, dass Sie das Suchmaschinen- Ranking Ihrer Anwendung verbessern. Ein suchmaschinenfreundlicher Link auf eine Produktseite zu einem Apple iPod könnte in einem Shop dann beispiels- weise so aussehen: 40
e-bol.net Die Praxis des MVC | 1.2 http://www.example.com/produkte/anzei gen/name/Apple%201Pod%20nano/ ean/8859091650494 Es gibt übrigens auch noch die Methode getParamsO, die Ihnen ein Array zurückgibt. Dieses Array beinhaltet dann alle Werte, die über die URL, GET und POST übergeben wurden. Auch hierbei gilt, dass bei Namensgleichheit der Wert erhalten bleibt, der zuerst gefunden wird. Die URL »überschreibt« GET und GET »überschreibt« POST. Zu jeder der get-Methoden gibt es auch ein Gegenstück, das mit set beginnt und Ihnen die Möglichkeit liefert, einen Wert zu setzen. Übrigens ist es auch möglich, die übergebenen Werte direkt in Form von Eigen- schaften des Request-Objekts auszulesen. Ich hätte also auch einfach auf $request->ort zugreifen können. Allerdings würde ich davon abraten, da das noch weniger eindeutig ist. In diesem Fall wird zuerst geprüft, ob es einen URL-Parameter mit dem Namen gibt. Danach werden die Arrays $_GET, $_POST, $_COOKIE, REQUESTJJRI, PATH_INFO, $_SERVER und $_ENV abgefragt, ob es einen Schlüssel mit dem Namen gibt. Wobei REQUEST_URI und PATH_INFO natürlich keine Arrays sind. Es handelt sich dabei um Eigenschaften, die vom System auf verschiedenen Wegen gefüllt werden können. Der Hintergrund ist, dass $_SER- VER[ ’ REQUEST_URI' ] bzw. $_SERVER[ ’ PATH_INFO' ] nicht immer korrekt belegt sind. Damit kennen Sie auch schon die grundlegenden Funktionalitäten des MVC. Zugegebenermaßen fehlt noch das M, nämlich das Model. 1.2.6 Das Model Eigentlich bedürfte das Model keiner eigenen Überschrift, da Sie es völlig frei definieren können. Hier gibt es keine wirklichen Vorgaben. Das hat den Grund darin, dass es hier keinerlei »Magie« gibt, die das Model automatisch einbindet oder automatisch Daten daraus bezieht. Das heißt, Sie können eine eigene Klasse erstellen, diese manuell im Action Controller einbinden und wie gewohnt nut- zen. Dennoch möchte ich ein paar Punkte hierzu anmerken. Innerhalb der Verzeich- nisstruktur ist das Verzeichnis models zum Speichern der Model-Klassen bzw. -Dateien gedacht. Natürlich könnten Sie diese auch an einem anderen Ort spei- chern, allerdings es ist doch sehr hilfreich, wenn Sie sich an diese Empfehlung halten. Eine klare Struktur hilft, das Dritte Ihren Code verstehen und sorgt auch dafür, dass Sie nicht lange suchen müssen, um die Komponenten zu finden. 41
e-bol.net 1 | Der Model View Controller Der zweite Punkt, den ich Ihnen empfehlen möchte, ist, dass Sie eine eigene Exception-Klasse für Ihr Model nutzen. Diese muss nicht sonderlich umfangreich sein. Eine Variante wie Model_Exception extends Exception {} reicht schon völlig aus. Damit haben Sie dann die Möglichkeit, diejenigen Excep- tions, die zum Beispiel beim Datenbankzugriff oder Ähnlichem entstehen, in eine eigene Exception zu überführen. Damit meine ich ein Konstrukt wie dieses: try { // Datenbankzugriff l catch (Zend_Db_Exception $e) { throw new Model_Exception ($e->getMessage(), $e->getCode()); l Dies gibt Ihnen den Vorteil, sich bei der Nutzung des Models innerhalb des Action Controllers keinerlei Gedanken darum machen zu müssen, welche Klas- sen innerhalb des Models genutzt werden. 1.2.7 Error Handling Was Sie bisher über den MVC wissen, bringt Sie schon recht weit und Sie können schon eine ganze Menge damit umsetzen. Was passiert aber, wenn ein Fehler auftritt? Die erste Stelle, an der ein Fehler auftreten kann, betrifft die Situation, dass ein Controller oder eine Action angesprochen werden, die nicht existieren. In der momentanen Konfiguration wirft das System eine Zend_Control l er_ Dispatcher_Exception, wenn Sie versuchen, einen Controller anzusprechen, den es nicht gibt, und eine Zend_Controller_Action_Exception, wenn es zwar den Controller, aber nicht die entsprechende Action gibt. Hier sind verschiedene »Schrauben« vorhanden, an denen Sie »drehen« können, um das System möglichst zuverlässig zu machen. Zunächst ist zu überlegen, was passieren soll, wenn ein Controller angesprochen wird, den es nicht gibt. Um solche Fälle möglichst elegant zu lösen, empfiehlt es sich, im Front Controller den Parameter useDefaul tControl l erAl ways mit dem Wert true zu belegen: $fc->setParam(’useDefaultControllerAlways', true); 42
e-bol.net Die Praxis des MVC | 1.2 Sollte der Front Controller nun mit einem Action Controller aufgerufen werden, den es nicht gibt, wird die Anfrage automatisch an den Index-Controller weiter- geleitet. Das ist aber nur die halbe Miete. Der Aufruf http://127.0.0.1/mich- gibtsnicht stellt kein Problem dar. Er landet bei der indexAction von Index- Controller. Der Aufruf http://127.0.0.'l/michgibtsnicht/michauchnicht resultiert aber nach wie vor in einer Exception, weil es die Methode mi chauchni chtActi on nicht gibt. Um dieses Problem zu lösen, können Sie die magische Methode __cal 1 () nutzen. Sie ist seit PHP 5 verfügbar und wird immer dann aufgerufen, wenn eine Methode einer Klasse aufgerufen werden soll, die nicht implementiert ist. Falls also eine unbekannte Action angesprochen werden soll, wird_cal 1 () aufgerufen. Man könnte in der Methode auch einfach eine Exception werfen, aber das würde Sie nicht wirklich weiter bringen, da Sie nur eine Exception gegen eine andere getauscht hätten. Es ist aber auch möglich, die Anfrage an eine andere Methode weiterzuleiten, die Anfrage also zu »forwarden«. Dafür ist die Methode _forward() deklariert. Eine solche Implementation könnten Sie wie folgt umset- zen: dass Indexcontroller extends Zend_Control 1 er_Action { public function indexAction() { // normal implementierte indexAction // Wird aufgerufen wenn die gesuchte // Methode nicht deklariert ist public function ___cal1($methode, Sparameter) { // Endet der Name der aufgerufenen Methode auf Action? if ('Action' ==— substr($methode, -6)) // Dann ein Forward auf indexAction $ t h is->_forward('i ndex’); 1 el se throw new Exception("Methode Smethode existiert nicht"): } } Listing 1.5 Nutzung der magischen Methode_call() 43
e-bol.net 1 | Der Model View Controller Die Methode___cal l () prüft, ob der Name der Methode, die aufgerufen werden soll, auf Action endet. Ist das der Fall, erfolgte der Aufruf vom Dispatcher und daraus resultiert, dass jemand eine falsche URL genutzt hat. Sollte der Aufruf nicht auf Action enden, wird es sich um einen Programmierfehler handeln, der natürlich nach wie vor in einer Exception resultieren sollte. Mit der Methode _forward() können Sie die Anfrage auch an einen anderen Action Controller und sogar an ein komplett anderes Modul weiterreichen. Der erste Parameter ist dabei der Name der Action, wie Sie gesehen haben. Als zwei- ten Parameter können Sie den Namen des Controllers angeben. Auch hierbei gilt, dass nur der eigentliche Name angegeben wird. Also beispielsweise foo für den FooControl ler. Als dritten Parameter können Sie dann noch den Namen eines Moduls angeben. So schön diese Technik auch sein mag - ich muss an dieser Stelle dennoch eine deutliche Warnung anbringen. Nutzen Sie Parameter, die über die URL überge- ben werden, kann diese Technik schnell verwirren. Bei der folgenden Vorge- hensweise würden beide URLs bei der Methode indexAction() aus dem Index - Control ler landen: http:/ /127.0.0.1/i ndex/index/ort/Bi elefeld/plz/33602 http://127.0.0.1/ort/Bielefeld/plz/33602 Die erste URL ist eindeutig und unproblematisch. Die zweite hingegen würde beim Auslesen der Daten mit getParams!) diese Informationen liefern: array(4) { ["control1 er"]=> string(3) "ort" ["action"]=> string(9) "Bielefeld" E"plz"]=> string(5) "33602" E’module"]=> string(7) "default" } Es wäre eventuell möglich, die Werte im Request-Objekt mithilfe von setPa- ram() neu zu setzen, um eine Verarbeitung möglich zu machen. Wenn Sie das tun, sollten Sie dabei aber im Hinterkopf behalten, dass es mehrere Möglichkei- ten gibt, warum der Aufruf bei_cal 1 () gelandet ist. 44
e-bol.net Die Praxis des MVC | 1.2 Neben der Methode _forward() ist auch die Methode _redi rect() deklariert. Hiermit können Sie ein echtes Redirect auf eine andere URL realisieren. Sie bekommt als ersten Parameter die Ziel-URL übergeben. Diese können Sie absolut angeben (was zu empfehlen ist) oder auch relativ. Das heißt, Sie können hier auch lediglich $this->_redirect('/i ndex/1ndex'); nutzen. Wichtig ist dabei, dass Sie bei einer relativen Angabe mit einem Slash am Anfang arbeiten sollten, weil Sie sonst schnell in einer Redirect-Endlosschleife landen. Ein Redirect hat gegenüber einem Forward den Vorteil, dass der Client den Sta- tuscode 302 (Moved Temporarily) mitgeteilt bekommt. Haben Sie die URLs auf Ihrem Server umgestellt, und wollen Sie den Suchmaschinen mitteilen, dass die Struktur sich dauerhaft geändert hat, so übergeben Sie _redi rect() als zweiten Parameter array (’code'=>'301'). Damit wird dann der HTTP-Code 301 (Moved Permanently) mit gesendet. Nun kann es aber immer noch passieren, dass ein Fehler bei der Ausführung des Codes auftritt. Auch in diesem Zusammenhang gibt es wieder zwei »Schrauben«, die man justie- ren kann. Zum ersten ist da die Methode throwExceptions(). Diese haben Sie schon kurz am Anfang des Kapitels kennengelernt. Normalerweise unterdrückt die MVC-Implementation die Ausgabe von Exceptions. Für die Entwicklung ist es allerdings sicherlich hilfreich, dass Exceptions geworfen werden. Für den pro- duktiven Einsatz sollten Sie das natürlich wieder ändern. Am besten übergeben Sie der Methode den Wert false: $fc->throwExceptions(false); Alternativ können Sie die Zeile auch einfach löschen. Tritt jetzt eine Exception auf, bleibt das Browserfenster einfach leer, was natürlich auch nicht so schön ist. Um das zu verhindern, können Sie das Error-Handling-Plug-in nutzen. Und zwar habe ich das Plug-in Front Controller mit dieser Zeile ausgeschaltet: $fc->setParam('noErrorHandler ’, true); Übergeben Sie hier fal se (beachten Sie die doppelte Verneinung), ist das Plug-in wieder aktiv. Wenn jetzt ein Fehler auftritt, wird automatisch die errorAction im ErrorControl 1 er aufgerufen. Dabei handelt es sich um einen ganz normalen Controller, den Sie selbst implementieren müssen. Außerdem müssen Sie auch ein Template für ihn anlegen. Dieses muss unter dem Namen error.phtml im Ordner error unterhalb von views/scripts abgespeichert werden. 45
e-bol.net 1 | Der Model View Controller Damit haben Sie nun die Möglichkeit, eine schicke Fehlermeldung für den Benut- zer auszugeben. Nur würden Sie als Betreiber der Website nie merken, dass es ein Problem gibt, weil die Exceptions alle vom System gefangen werden. Genau genommen speichert das Error-Handling-Plug-in sie im Response-Objekt. Das ist normalerweise dazu da, die Antwort für den Client zu verwalten. Allerdings kön- nen Sie das Response-Objekt auch aus dem Action bzw. aus dem Error Controller heraus auslesen und dann die dort enthaltenen Exceptions verarbeiten. In einer ganz einfachen Variante könnte ein solcher Error Controller so aussehen: requi re_once "Zend/Controller/Action.php"; dass ErrorControl ler extends Zend_Control ler_Action { public function errorAction() { $view = $this->initView(): $response = $this->getResponse(); $exceptions = $response->getException(); $ausgabe : foreach (texceptions as $exception) $ausgabe[]=$exception->getMessage(); l $view->ausgabe = $ausgabe: } Listing 1.6 Ein einfacher Error-Controller Das Response-Objekt wird hier mit der Methoden getResponse() ausgelesen. Die Exceptions, die darin enthalten sind, stellt die Methode getException() zur Verfügung. Da es schnell passieren kann, dass mehrere Fehler auftreten, liefert die Methode ein Array mit Exceptions zurück. Der Body-Abschnitt des dazugehörigen Templates könnte beispielsweise so aus- sehen: <body> Die folgenden Fehler traten auf:<br> <ul > < ? php foreach ($this->ausgabe as $ausgabe) ( 46
e-bol.net Die Praxis des MVC | 1.2 echo "<li >".$thi s->escape(Sausgabe). "</li >"; } ?> < / ul > </body> Listing 1.7 Das Template error.phtml Zugegebenermaßen ist das keine Variante, die Sie für eine produktive Anwen- dung nutzen sollten, weil Sie sonst ja auch gleich direkt die Exception ausgeben könnten. Es geht an dieser Stelle viel mehr darum, Ihnen die Funktionsweise zu zeigen. Für eine produktive Anwendung würde es sich anbieten, die Fehlermel- dungen in eine Log-Datei zu schreiben, den Administrator per E-Mail zu benach- richtigen und für den Benutzer eine »kundenfreundliche« Fehlermeldung auszu- geben. In diesem Zusammenhang sollten Sie auch einen Blick auf die Klasse Zend_Log werfen. Hinweisen möchte ich Sie auch noch darauf, dass Sie im Manual des Zend Frame- works unter der Adresse http://framework.zend.com/manual/en/zend.controller. plugins.html eine etwas ausgefeiltere Variante zur Fehlerbehandlung finden. 1.2.8 Fortgeschrittene Techniken In den folgenden Abschnitten finden Sie noch einige weitergehende Techniken, die Sie beim Umgang mit den Komponenten unterstützen. Action-Controller Die Klassen, die Sie als Action Controller nutzen, sollten den Konstruktor nicht überschreiben. Sollten Sie den Konstruktor dennoch einmal überschreiben wol- len, so beachten Sie bitte, dass Sie den Konstruktor des Eltern-Objekts mit parent::___construct($this->getRequest(). $ this->getResponse(), Sthis->getInvokeArgs()) aufrufen. Allerdings haben Sie auch eine andere Möglichkeit, Standardaufgaben auszuführen. Dazu können Sie die Methode ini t() implementieren. Diese wird vom Konstruktor automatisch ausgeführt: public function initO { // View initialisieren etc. } 47
e-bol.net 1 | Der Model View Controller Wie Sie schon gesehen haben, können Sie aus einem Action Controller heraus auf das Response-Objekt zugreifen, welches für die Antwort an den Client zuständig ist. Grundsätzlich können Sie die meisten Punkte, die mit der Antwort verbun- den sind, auch beeinflussen. Ich möchte an dieser Stelle nicht auf den kompletten Funktionsumfang des Response-Objekts eingehen; Sie können ihn unter der Adresse http://framework. zend.com/manual/en/zend.controller.response.html nachlesen. Erwähnen möchte ich aber, wie Sie einen Header versenden. Das kann in einigen Fällen sehr hilf- reich sein. Einen Header können Sie mit der Methode setHeader() übergeben. Sie bekommt als ersten Parameter den Namen des Headers übergeben und als zweiten den dazugehörigen Wert. Da ein Header nur dann gesetzt werden kann, wenn noch keine Daten zum Client geschickt wurden, sollten Sie vor dem Setzen des Headers mit canSendHeaders() prüfen, ob dieser Schritt möglich ist. Falls Sie aus einer Methode in einem Action Controller einen Header senden wollen, könnte das wie folgt umgesetzt werden: public function indexActionC) { $response = $this->getResponse(): if (true —== $response->canSendHeaders()) { $response->setHeader('Content-Type', 'text/html; charset=utf-8’): // Weiterer Code } Mit diesen Zeilen würde der Antwort ein UTF-8-Header mitgeschickt. Die Methode canSendHeadersO kann übrigens auch sofort eine Exception werfen, wenn die Header nicht mehr gesetzt werden können. Falls Sie das wünschen, übergeben Sie ihr true. Die Methode setHeader() akzeptiert auch noch einen booleschen Wert als weiteren Parameter. Übergeben Sie dort true, wird ein Hea- der mit gleichem Namen, der eventuell schon gesetzt ist, überschrieben. Stan- dardmäßig ist das allerdings nicht der Fall. Bei der Einführung der Templates hatte ich erwähnt, dass Sie nicht unbedingt für jede Action ein eigenes Template anlegen müssen. Dabei bin ich Ihnen aber bis- her die Antwort schuldig geblieben, wie Sie das realisieren. Ich empfehle Ihnen, dass Sie zumindest für jeden Action Controller ein Template behalten. Das ver- bessert die Übersichtlichkeit der Anwendung. Möchten Sie aber von einer Action ein anderes Template nutzen, ist das kein Problem. Um so etwas zu beeinflussen, ist ein sogenannter Helper, nämlich ViewRenderer, vorgesehen. Mit ihm haben 48
e-bol.net Die Praxis des MVC | 1.2 Sie die Möglichkeit, verschiedene Parameter zu verändern, welche die Darstel- lung und die Nutzung der Templates betreffen. Zuerst benötigen Sie das entspre- chende Objekt, das Sie mit der statischen Methode getStaticHelper() aus der Klasse Zend_Control l er_Action_Hel perBroker auslesen. Sobald Sie das Objekt haben, können Sie die Methode setRender () aufrufen und ihr den Namen eines anderen Templates, genau genommen den Namen einer anderen Action, überge- ben. public function indexAction() { $viewRenderer = Zend_Controller_Action_HelperBroker:: getStaticHelper('viewRenderer'); $ v iewRenderer->setRender('allgemei n'); } Listing 1.8 Nutzung eines anderen Templates In diesem Beispiel wird also nicht mehr das Template index.phtml genutzt, son- dern allgemein.phtml. Plug-ins Den Begriff Plug-in haben Sie in diesem Kapitel ja schon gelesen. Bei Plug-ins handelt es sich um Methoden, die an einem bestimmten Punkt der Verarbeitung automatisch ausgeführt werden. Mit Plug-ins können Sie eine ganze Menge Dinge erledigen lassen, die Sie sonst mehrfach manuell ausführen müssten. Einige Plug-ins bringt das Zend Framework schon mit, andere können Sie selbst implementieren. Diejenigen, die Sie selbst implementieren können, werden an einer bestimmten Stelle in den Verarbeitungsprozess eingehängt oder »einge- hakt«. Daher spricht man an dieser Stelle auch von Hooks. Ist eine dieser Stellen erreicht, wird das entsprechende Plug-in automatisch ausgeführt. Ich möchte hier nicht auf alle Stellen eingehen, an denen Sie ein Plug-in aufrufen lassen kön- nen. Zwei Stellen finde ich aber sehr hilfreich. Das ist zum ersten preDispatch und zum anderen postDi spatch. Hierbei handelt es sich um den Zeitpunkt direkt vor sowie direkt nach dem Dispatching. Oder anders formuliert, vor und nach dem Ausführen der Action aus dem Controller. Müssten Sie beispielsweise bei jeder Seite einen UTF-8-Header mitsenden, würde es sich anbieten, dies mit dem preDi spatch-Plug-in zu tun. Die Plug-ins werden alle in einer Klasse implementiert, die innerhalb des Front Controllers beim System angemeldet werden sollte. 49
e-bol.net 1 | Der Model View Controller Ein Plug-in, das jede Seite mit einem UTF-8-Header versieht, könnte so aussehen: requi re_once 'Zend/Controller/Front.php’; require_once 'Zend/Controller/Plugin/Abstract.php'; dass MeinePlugins extends Zend_Control ler_Pl ugin_Abstract { public function preDispatch(Zend_Controller_Request_Abstract Srequest) { $response = $this->getResponse(); if (true ==— $response->canSendHeaders()) $response->setHeader('Content-Type', ’text/html; charset=utf-8'): } } // normale Implementation des Front-Controllers $fc->regi sterPlugi n(new MeinePlugins()): $fc->di spatch(); Listing 1.9 Implementation eines Plug-ins Die Plug-ins werden alle in einer Klasse zusammengefasst. Diese Klasse muss die abstrakte Klasse Zend_Control l er_Pl ugin_Abstract implementieren. Jedes Plug-in muss das Request-Objekt als Parameter akzeptieren. Am Namen des Plug- ins erkennt der Plug-in-Broker, an welcher Stelle es ausgeführt werden muss. Wie bereits gesagt, wird das preDi spateh-Plug-in stets vor dem Dispatching und somit vor der Verarbeitung der Action ausgeführt. Nachdem die Klasse entsprechend implementiert ist, muss nur noch ein Objekt von ihr abgeleitet werden, das mithilfe der Methode regi sterPl ugin() beim Plug-in-Broker angemeldet wird. Wollten Sie noch ein postDi spatch-Plug-in nutzen, würde dies einfach mit der oben dargestellten Klasse implementiert. 1.2.9 Ein Beispiel Da sicher das eine oder andere beim MVC ein wenig abstrakt ist, folgt jetzt noch ein kleines Beispiel. Es ist wirklich nur eine ganz kleine Anwendung, die noch einmal die Prinzipien verdeutlichen soll. 50
e-bol.net Die Praxis des MVC | 1.2 Und zwar dient die folgende »Anwendung« dazu, Telefonnummern zu verwal- ten. Die Möglichkeiten beschränken sich allerdings darauf, neue Telefonnum- mern anzulegen und bestehende anzuzeigen. Die dazu verwendete Tabelle hat den folgenden Aufbau: CREATE TABLE 'telefonliste' ( 'id' int(ll) NOT NULL AUTO_INCREMENT, 'name' varchar(lOO) DEFAULT NULL, 'telefon' varchar(lOO) DEFAULT NULL, PRIMARY KEY ('id' ) ) ENGINE=MyISAM DEFAULT CHARSET=latinl Listing 1.10 Datenbanktabelle Der Front Controller Der Front Controller der Anwendung macht im Endeffekt das, was Sie schon kennengelernt haben. Hier gibt es keine Besonderheiten: requi re_once ’Zend/Controller/Front.php’; // Controller-Instanz auslesen $fc = Zend_Controller_Front::getlnstance(): // Verzeichns setzen $fc->setControllerDi rectoryi’/var/www/appl/controllers’): // Nutzung von Views unterdrücken $fc->setParam(’noViewRenderer’, false): // So einstellen, dass Ausnahmen geworfen werden $fc->throwExceptions(false); // Error Händler ausschalten $fc->setParam(’noErrorHandl er’ , false): $fc->setParam(’useDefaultControllerAlways’, true); $fc->di spatch(); Listing 1.11 Der Front Controller In diesem Front Controller wird zwar das Error Handling eingeschaltet, aber ich werde darauf verzichten, hier den Error Controller vorzustellen. Index Controller Interessanter, aber auch unspektakulär ist der Index Controller umgesetzt. Er soll in diesem Beispiel nur dafür genutzt werden, ein statisches Hauptmenü auszuge- ben. Daher muss die Action nichts machen. Trotzdem muss die Action deklariert werden: 51
e-bol.net 1 | Der Model View Controller requi re_once 'Zend/Controller/Action.php'; dass Indexcontroller extends Zend_Control ler_Action { // Die Action muss nichts machen public function indexAction!) { } // Falsche Aufrufe abfangen public function ___call($methode, Sparameter) { if ('Action' === substr!$methode, -6)) $this->_forward('i ndex’); l throw new Exception!"Methode $methode existiert nicht”); } Listing 1.12 Der Index Controller Auch wenn der Controller selbst nicht sonderlich viel tut, muss das Template ein wenig mehr Inhalt haben. Der Body-Bereich des Templates sieht folgendermaßen aus: <body> <h2>Willkommen zur Telefonliste!</h2> Was darf’s sein? <ul > <l iXa href='/neu' >Neue Nummer anlegen</a></l i> <liXa href='/anzeigen ’ >Al l e Nummern anzei gen</aX/l i > </ul> </body> Listing 1.13 Template für den Index-Controller Hier sind also nur zwei statische Links auf die beiden Controller der Anwendung enthalten. Eine Action habe ich nicht angegeben; hier wird einfach die Index- Action genutzt. Der Controller »Neu« Der Controller »Neu« ist dafür zuständig, neue Telefonnummern zu speichern. Es gibt hier also zwei Aktionen, die zu erledigen sind: Das Ausgeben des Formulars und das Speichern der Daten. Daher hat der Controller auch zwei entsprechende Methoden zuzüglich der Methode___cal l (). Auf deren Darstellung habe ich hier 52
e-bol.net Die Praxis des MVC | 1.2 verzichtet. Dafür finden Sie aber die Methode i ni t(), die dafür zuständig ist, das Model zu initialisieren: requi re_once "Zend/Controller/Action.php"; requi re_once '/appi/models/AdressenDbModel.php'; dass NeuController extends Zend_Control ler_Action { pri vate Smodel = null; // initialisiert das Model public function init() { $this->model = new AdressenModel(): } // Initialisiert die Ausgabe des Formulars public function indexAction() { $view = $this->initView(); $ v iew->valueName = ''; $view->disableName = false: $view->valueTelefon = $view->disableTelefon = false: $view->showSubmit = true: $ v iew->meldung = ’’: } public function speichernAction() { $request = $this->getRequest(); $view = $this->initView(): $viewRenderer = Zend_Controller_Action_HelperBroker:: getStati cHelper('vi ewRenderer'); $viewRenderer->setRender(* i ndex'): // Feld fuer Namen initialisieren $name = $request->getPost('name’): $view->valueName = $name: $view->disableName = true: $telefon = $request->getPost(’telefon’): $view->valueTelefon = $telefon; $view->disableTelefon = true: $view->showSubmit = false: $view->meldüng = 'Vielen Dank, die folgenden 53
e-bol.net 1 | Der Model View Controller Daten wurden gespeichert!'; $daten = array ('name' => $name, ’telefon' => Stelefon); $this->model->datenSpei ehern (Sdaten); } I Listing 1.14 Controller zum Anlegen neuer Daten Sicher hatten Sie hier etwas anderen Code erwartet. Es wäre ein einfaches gewe- sen zwei Templates zu nutzen - eins mit einem Formular und das andere mit einer Ausgabe die dem Benutzer mitteilt, dass die Daten gespeichert wurden. Da ich aber nur ein Template nutzen wollte wird das Template durch die Aktionen initialisiert. Für die beiden Eingabefelder gibt es dazu jeweils zwei Eigenschaften. Im Fall des Feldes für den Namen sind das val ueName und di sabl eName. Mit der ersten Eigenschaft kann ein Wert für das Value-Attribut übergeben werden und mit der zweiten kann gesteuert werden ob das Feld aktiv ist. Wird hiermit true übergeben, dann wird das Attribut di sabl ed in das Tag eingefügt. Des Weiteren ist eine Eigenschaft namens showSubmit vorgesehen mit deren Hilfe Sie den Submit-Button ausblenden können. In diesem Beispiel wird das Formular also zunächst zur Eingabe der Daten genutzt. Nach dem Abschicken sorgt die Methode speichernActioni) dafür, dass die Daten an das Model übergeben werden. Darüber hinaus werden sie auch noch einmal im Formular ausgegeben, wobei die Felder nicht aktiv sind und der Submit-Button ausgeblendet wird. So kann der Benutzer noch einmal sehen was er eingegeben hatte. Die Nutzung des Formulars, wie ich sie umgesetzt habe, ist nicht sonderlich ele- gant. Sollten Sie öfter mit Formularen zu tun haben, so werfen Sie einmal einen Blick auf die View-Helper und Zend_Fi l ter_Input. Wie Sie hier aber schön erkennen, muss dieser Teil der Anwendung sich nicht weiter um die Daten kümmern. Er übergibt sie einfach an das Model, das die Arbeit leistet. Der Body-Bereich des Templates, das diese beiden Methoden nutzen, ist folgen- dermaßen aufgebaut: <body> <form action=/neu/speichern" method="post"> <? php echo $this->meldüng; 54
e-bol.net Die Praxis des MVC | 1.2 ?> <table> <trXtd>Name</td> CtdXinput type="text" name-’name" value ='<?php echo $this->valueName; ?>' <?php if ($this->disableName) lecho ’disabled’;) ?> > </td> </1 r> <trXtd>Tel efon</td> CtdXinput type="text" name="telefon" value ='<?php echo $this->valueTelefon; ?>' <?php if ($this->disableTelefon) (echo ’disabled’:) ?> > </td> </tr> CtrXtd colspan="2" al ign=’,center"> <?php if (true — $this>showSubmit) I echo xinput type='submit' value='Spei ehern’>"; 1 ?> </td> </tr> </1a ble> </form> </body> Listing 1.15 Template zum Anlegen der Daten X X Telefonliste CD Name Spongebob Squarepants Telefon +1021) 953222 ( Speichern ) Abbildung 1.2 Eingabe der Daten Nun fehlt noch der Teil, der die Daten wieder ausgibt. 55
e-bol.net 1 | Der Model View Controller Der Controller »Anzeigen« Dieser Controller bekommt die Daten vom Model übergeben und stellt sie dar. Die Daten werden als Array übergeben, sodass sie im Template mit einer for- each-Schleife ausgegeben werden können: requi re_once "Zend/Controller/Action.php"; requi re_once '/appi/models/AdressenDbModel.php'; dass AnzeigenController extends Zend_Controller_Action { private $model = null: public function init() { $this->model = new AdressenModel(): public function indexAction() { $view = $this->initView(): $daten = $this->model->datenl_esen(); $view->daten = Sdaten: } Listing 1.16 Der Controller zum Anzeigen der Daten Auch hier ist nicht mehr viel zu erläutern. Das Template ist ähnlich einfach auf- gebaut: <body> < ? php echo "Ctable border=’1'>"; foreach ($this->daten as $zeile) { echo "<trXtd>": echo $ t hi s->escape($zei1e[’name']): echo "</td><td>": echo $ t hi s->escape($zei1e[’telefon’]); echo "</tdX/tr>"; } echo "</table>": ?> </body> Listing 1.17 Das Template für die Ausgabe 56
e-bol.net Die Praxis des MVC | 1.2 Telefonliste aWiOÖ » Homer Simpson +1 (3735)4810278 Monty Bums +1 (3735) 373533 Patrick Star +1 (921)98151 Spongcbob Squarcpants +1 (921)953222 4 Abbildung 1.3 Die Darstellung der Daten im Browser Das Model Zu guter Letzt fehlt noch das Model, das in den beiden Controllern ja schon genutzt wurde. Ich habe hier auf Zend_Db zurückgegriffen, um die Komponente möglichst einfach und schnell umsetzen zu können. Auch wenn Sie sich vielleicht noch nicht mit Zend_Db auskennen, denke ich, dass die beiden Methoden doch gut zu verstehen sind: requi re_once('Zend/Loader.php'); Zend_Loader::loadClass('Zend_Db’); dass AdressenModel { pri vate $db = null : public function ___construct() { // Daten für den Zugriff auf die Datenbank $optionen = array( 'host’ => *127.0.0.1' , ’username' => 'root’, ’password’ => '’, ’dbname’ => 'test’ ); // Objekt ableiten $this->db = Zend_Db::factory(’mysqli',$optionen); * Speichert Daten in der Datenbank * @param array $daten * @return true 57
e-bol.net 1 | Der Model View Controller */ public function datenSpeichern (array $daten) { $sql = "INSERT INTO telefonliste (name, telefon) VALUES (?. ?)"; // Befehl vorbereiten $stmt = $this->db->prepare($sql); // Befehl ausführen $stmt->execute(Sdaten); * Liest alle Daten aus Tabelle * und gibt sie als Array zurück * @return array */ public function datenLesen () { $sql = ’SELECT * FROM telefonliste'; return $this->db->fetchAll($sql); } Listing 1.18 Das Model zum Zugriff auf die Datenbank Sie sehen, an dem Model ist wirklich nichts Besonderes. Es ist einfach nur eine normale Klasse. 58
e-bol.net Nukular, das Wort heißt nukular.... - Homer Simpson 2 Datenbankzugriff mit Zend.Db Die meisten Anwendungen nutzen heutzutage eine Datenbank, um Daten zu ver- walten. Nun könnten Sie in Ihrer Applikation, die Sie mit dem Zend Framework erstellen, einfach die Datenbankfunktionen nutzen, die Sie schon immer genutzt haben. Der unschöne Nebeneffekt ist allerdings, dass Sie, so lange Sie keine eige- nen Funktionen erstellen, redundanten Code haben, weil Sie beispielsweise Feh- ler mehrfach abfangen müssen. Der zweite Punkt ist, dass Sie nicht datenbank- unabhängig arbeiten. Wenn Sie also Ihre Anwendung auf eine andere Datenbanksoftware umstellen wollen, müssen Sie den gesamten Code überarbei- ten. Diese und ähnliche Probleme versuchen die Zend_Db-Klassen auszumerzen. Die meisten Informationen in diesem Kapitel beziehen sich auf die Nutzung einer MySQL-Datenbank, da diese bei Webanwendungen sicher die größte Verbrei- tung besitzt. 2.1 Datenbankunabhängigkeit Einer der Gründe, warum man einen Datenbank-Layer einsetzt, liegt darin, dass man unabhängig von der Datenbank bleiben möchte. Sie können somit mit sehr einfachen Mitteln von einer Datenbank zu einer anderen migrieren. Allerdings muss man fairerweise dazu sagen, dass das eher die Theorie ist und sich in der Praxis nicht so einfach umsetzen lässt. Dabei stellt sich natürlich die Frage, warum das so ist. Schließlich nutzen die Datenbanken doch alle SQL. Ja, zwar nutzen sie alle SQL, aber in den meisten Fällen bieten sie alle mehr Möglichkei- ten als das, was in reinem SQL vorgesehen ist. Lassen Sie mich dies an einem Beispiel erläutern. Sicher kennen Sie in MySQL die Möglichkeit, eine Spalte mit dem Attribut auto_increment zu versehen. Damit wird der Integer-Wert der Spalte bei jedem INSERT um 1 erhöht. Gerade für das Generieren einer ID ist das eine sehr beliebte Funktionalität. Nutzen Sie diese Funktionalität, so sind Sie allerdings an MySQL gebunden, da es sich hierbei um 59
e-bol.net 2 | Datenbankzugriff mit Zend_Db ein MySQL-Feature handelt, das nicht in SQL deklariert ist. Andere Datenbank- systeme nutzen an dieser Stelle Sequenzen, die MySQL aber nicht kennt. Was nun also tun, um möglichst flexibel zu bleiben? Die Lösung ist einfach: Sie legen eine zusätzliche Tabelle mit einer Spalte an, die nur einen Integer-Wert enthält, nämlich die aktuelle ID. Möchten Sie in der Tabelle, welche die eigentlichen Daten speichert, einen Wert einfügen, so selektieren Sie zunächst die ID aus der »ID-Tabelle«. Die ID-Tabelle sollte allerdings zuvor gesperrt werden, damit kein anderer Prozess dieselbe ID ausliest. Nach dem Auslesen führen Sie ein Update auf die Tabelle aus, wobei der enthaltene Wert um 1 erhöht wird. Nachdem das erfolgt ist, können Sie die Tabelle wieder freigeben und die Daten, die Sie eigent- lich abspeichern wollten, in die Daten führende Tabelle schreiben. Neben der Tatsache, dass der programmiertechnische Aufwand hier natürlich viel größer ist, hat diese Vorgehensweise auch zur Folge, dass die Performance leidet. Was heißt das nun alles konkret? Wenn Sie mehr oder minder datenbankunab- hängig bleiben wollen, so müssen Sie zunächst analysieren, welche Funktionali- täten von den potenziellen Zieldatenbanken unterstützt werden. Danach müssen Sie einen gemeinsamen Nenner finden und die benötigten Funktionalitäten mit den vorhandenen Möglichkeiten implementieren. Alternativ können Sie natür- lich auch für jede mögliche Datenbank eine eigene Zugriffsklasse erstellen, was allerdings einen Datenbankabstraktionslayer wie Zend_Db weitgehend ad absurdum führen würde. 2.2 Nutzung von Zend_Db Um auf eine Datenbank zuzugreifen, ist es sinnvoll, mithilfe der statischen Methode factoryO aus der Klasse Zend_Db ein Objekt abzuleiten. Sie bindet automatisch die benötigte Klasse ein und gibt ein instantiiertes Objekt zurück. Alternativ könnten Sie auch selbst ein Objekt ableiten. Die Methode bekommt alle benötigen Werte als Parameter übergeben. Der erste Parameter ist dabei ein String, der definiert, welche der PHP-Datenbankfunktio- nen intern genutzt werden sollen. Sie können also selbst definieren, ob Sie lieber mit PDO oder mit den mysqli-Funktionen auf MySQL zugreifen wollen. Zulässig sind hier die Strings aus Tabelle Tabelle 2.1. String PHP-Funktionen/Datenbank ' Db2 ’ db2_* 'Mysqli ' mysqli_* Tabelle 2.1 Parameter für die factory-Methode 60
e-bol.net Nutzung von Zend_Db | 2.2 String PHP-Funktionen/Datenbank 'Oracle’ oci8_ ’Pdo_Ibm' PDO für IBM DB2 bzw. IBM IDS 'Pdo_Mssql' PDO für MS SQL-Server und Sybase 'Pdo_Mysql' PDO für MySQL 'Pdo_Oci' PDO für Oracle 'Pdo_Pgsql' PDO für PostreSQL 'Pdo_Sqli te’ PDO für SQLite Tabelle 2.1 Parameter für die factory-Methode (Forts.) Wie Sie sehen, werden die veralteten Funktionen mit dem Präfix mysql_ nicht mehr unterstützt. Der zweite Parameter, den die Methode factory() benötigt, ist ein Array mit den Informationen, die benötigt werden, um auf den Datenbankserver zuzugrei- fen. Das heißt, der Name bzw. die IP, der Benutzername u. Ä. müssen auf diesem Weg angegeben werden. Damit das Paket die Parameter eindeutig zuordnen kann, müssen Sie bestimmte Schlüssel verwenden. Diese finden Sie in Tabelle Tabelle 2.2. Schlüssel Beschreibung host Name oder IP-Adresse des Datenbankservers username Benutzername des Datenbankbenutzers, über den die Verbindung aufgebaut werden soll password Passwort des Datenbankbenutzers dbname Name der Datenbank, die genutzt werden soll port Damit können Sie optional einen anderen Port als den Standardport übergeben. options Mit diesem Schlüssel können Sie ein assoziatives Array übergeben, das allgemeine Optionen definiert. dri ver_opti ons Array mit Optionen, das direkt an den PDO-Konstruktor übergeben wird adapterNamespace Hiermit können Sie einen eigenen Namespace deklarieren, falls Sie eine eigene Datenbankklasse nutzen wollen. Tabelle 2.2 Optionen für die factory-Methode Auf die letzten drei Schlüssel in der Tabelle möchte ich noch kurz eingehen. Der Schlüssel opti ons kann auf ein assoziatives Array verweisen, welches allgemeine Optionen enthält, die für alle Datenbankadapter gültig sind. Innerhalb dieses Arrays dürfen zwei Schlüssel genutzt werden. Hierbei handelt es sich um die 61
e-bol.net 2 | Datenbankzugriff mit Zend_Db Konstanten Zend_Db::CASE_FOLDING und Zend_Db::AUTO_QUOTE_IDENTIFIERS. Mit der ersten können Sie festlegen, ob Sie das Case-Folding einschalten wollen. Normalerweise werden die Namen der Tabellenspalten so zurückgegeben, wie sie in der Datenbank deklariert wurden, das heißt, die Groß- und Kleinschrei- bung wird nicht geändert. Möchten Sie die Bezeichner komplett in Großbuchsta- ben erhalten, so übergeben Sie mit diesem Schlüssel den Wert Zend_Db: :CASE_ UPPER. Bevorzugen Sie die Namen in Kleinbuchstaben, so nutzen Sie die Kon- stante Zend_Db: :CASE_LOWER als Wert. Möglich ist auch die Konstante Zend_ Db: :CASE_NATURAL, die aber der Default-Einstellung entspricht. Mit ihr erhalten Sie die Namen in derjenigen Schreibweise, wie sie in der Datenbank verwendet wird. Der Schlüssel Zend_Db::AUTO_QUOTE_IDENTIFIERS akzeptiert die Werte true oder fal se und entscheidet darüber, ob Bezeichner wie Tabellen- oder Spalten- namen automatisch »gequotet«, also mit Backticks versehen werden sollen. Stan- dardmäßig geschieht dies durch Zend_Db nicht. Mit dem Schlüssel dri ver_opti ons können Sie ein Array übergeben, das Optio- nen enthält, die direkt an den Konstruktor der PHP-Klasse PDO übergeben wer- den. Das ist natürlich nur dann von Interesse, wenn Sie einen PDO-Adapter nut- zen. Weitere Informationen zu den Optionen finden Sie im PHP-Manual unter http://www.php.net/pdo. Der letzte Schlüssel adapterNamespace gibt Ihnen die Möglichkeit, einen selbst erstellten Datenbankadapter zu verwenden. Wie Sie selbst einen Adapter erstel- len, werde ich hier nicht erläutern, da alle relevanten Datenbanken durch das Zend Framework abgedeckt sind. Weitere Informationen hierzu können Sie dem Manual zum Zend Framework entnehmen. Wollen Sie also beispielsweise eine Verbindung zu einem lokal installierten MySQL-Server über die mysqli-Funktionen aufbauen, so könnte dies wie folgt aussehen: require_once 'Zend/Db.php'; Soptionen = array( 'host’ => '127.0.0.1’, 'username’ => 'root', 'password’ => '', ’dbname' => 'test' h $db = Zend_Db::factory(’mysqli',$optionen); Listing 2.1 Ableiten eines Objekts 62
e-bol.net Nutzung von Zend_Db | 2.2 Nach der Instantiierung enthält $db ein Objekt der Klasse Zend_Db_Adapter_My- sql i. Übergeben Sie einen anderen String aus Tabelle 2.1, erhalten Sie natürlich auch ein Objekt einer anderen Klasse. Eine vielleicht etwas ungewöhnliche Eigenschaft der Klassen ist, dass der Verbin- dungsaufbau zur Datenbank nicht sofort erfolgt. Die Daten werden zunächst nur gespeichert, und eine Verbindung wird erst dann aufgebaut, wenn es nötig ist. Möchten Sie explizit eine Verbindung aufbauen, müssen Sie die Methode get- Connection() aufrufen. Das hat den Vorteil, dass ein eventueller Fehler beim Verbindungsaufbau sofort erfolgt und nicht erst später, wenn man vielleicht nicht mehr damit rechnet. Die Zend_Db-Klassen bieten eine recht große Anzahl an verschiedensten Metho- den an, um auf Tabellen zuzugreifen. Um die dafür notwendigen SQL-Statements zu erstellen, kann es passieren, dass Sie die Werte oder Bezeichner von Tabellen oder Spalten »quoten« müssen, damit Sonderzeichen oder spezielle Namen nicht zu einem Problem werden. Da die Anforderungen in diesem Bereich von der ver- wendeten Datenbank abhängig sind, bietet Zend_Db entsprechende Methoden an. Um Werte, die in einem SQL-Statement genutzt werden, zu quoten, ist die Methode quote() vorgesehen. Sie bekommt als ersten Parameter den Wert über- geben, der genutzt werden soll. Als zweiten Parameter können Sie noch einen Datentyp übergeben. Dafür sind in der Klasse Zend_Db die Konstanten INT_TYPE (32 Bit Integer), BIGINT_TYPE (64 Bit Integer) und FLOAT_TYPE (Float oder Double) definiert. Nutzen Sie keinen zweiten Parameter, geht die Methode davon aus, dass es sich um einen String handelt. Bitte beachten Sie bei der Nutzung der Datentypen, dass die übergebenen Daten konvertiert werden. I NT_TYPE und FLOAT_TYPE nutzen die PHP-Funktionen int- val() und floatvalO zur Konvertierung, wohingegen bei BIGINT_TYPE ein regulärer Ausdruck genutzt wird, der auch eine hexadezimale Darstellung verar- beiten kann: Sstring = "Hallo 'Weit'!": echo $db->quote($string); // ’HalloXn VWeltV!’ echo $db->quote(”2 Bücher", Zend_Db::INT_TYPE); // 2 echo $db->quote("Bücher: 2", Zend_Db::INT_TYPE): // 0 echo $db->quote("OxFF Bücher", Zend_Db::BIGINT_TYPE); // OxFF echo $db->quote("OxFF Bücher", Zend_Db::INT_TYPE); // 0 Eine Variante der Methode quote() ist quotelnto(). Sie gibt Ihnen die Möglich- keit, einen Wert direkt in ein SQL-Statement einzufügen. Als ersten Parameter bekommt die Methode ein SQL-Statement übergeben, bei dem ein Wert durch ein Fragezeichen als Platzhalter ersetzt wurde. Der zweite Parameter ist der Wert, 63
e-bol.net 2 | Datenbankzugriff mit Zend_Db der escapet und anstelle des Fragezeichens in den SQL-String eingefügt werden soll. Als dritten Parameter können Sie eine der Konstanten angeben, die schon bei quote() vorgestellt wurden. Sstring = "Hallo 'Welt*!"; Ssql = "INSERT INTO daten(begruessung) VALUES (?)"; $sql_safe = $db->quotelnto(Ssql , $str1ng); // Ssql_safe enthält jetzt: // INSERT INTO daten(begruessung) VALUES (’HalloXn \'Welt\'!') Listing 2.2 Quoting von Daten Zusätzlich zu den eigentlichen Werten können Sie auch die Namen von Tabellen oder Spalten quoten lassen. Neben der Möglichkeit, dies global über die Optio- nen einzuschalten, sind hierzu mehrere Methoden deklariert. Eine universell verwendbare Methode ist quoteldenti fi er(). Sie kann alle Arten von Tabellen- bezeichnern (Tabellennamen, Spaltennamen etc.) quoten und erhält entweder einen String oder ein Array übergeben und liefert die enthaltenen Daten korrekt formatiert zurück. Bei einem Array werden die enthaltenen Strings einfach hin- tereinander gruppiert, sodass Sie beispielsweise die Datenbank und die Tabelle nacheinander angeben können: Stabelle = $db->quoteldentifier(’plz'); Sspalte = Sdb->quoteldentifier(array('plz*, ’id')); Ssql = "SELECT Sspalte FROM Stabelle": // Ssql enthält jetzt SELECT 'plz'.'id' FROM 'plz' Listing 2.3 Quoten eines Identifiers In vielen Fällen ist es sinnvoll oder sogar notwendig, mit einem Alias zu arbeiten. Das bedeutet, dass Sie eine Tabelle oder eine Spalte nicht mehr unter ihrem eigentlichen Namen ansprechen, sondern einen Aliasnamen vergeben. Auch da- für sind zwei spezielle Methoden, nämlich quoteCol umnAs() und quoteTa- bleAsO, vorgesehen. Beide bekommen als ersten Parameter den Namen der Spalte bzw. der Tabelle als String oder Array übergeben. Der zweite Parameter ist der Aliasname, der genutzt werden soll: Stabelle = Sdb->quoteTableAs(’plz Stabei 1enalias); Sspalte = $db->quoteColumnAs(array($tabellenalias, ’id’), ’s’); Sspalte_where = Sdb->quoteldentifier( arraylStabellenalias.'id’)): Ssql = "SELECT Sspalte FROM Stabelle WHERE Sspalte_where > 100”: 64
e-bol.net Nutzung von Zend_Db | 2.2 // $sql enthält jetzt: // SELECT 't'.'id' AS 's' FROM 'plz' AS 't' // WHERE 't'.'id' > 100" Listing 2.4 Nutzung eines Alias Nachdem Sie nun wissen, wie Sie ein SQL-Statement erstellen, ist noch zu klären, wie Sie es an den Server senden. Es gibt eine ganze Reihe von Möglichkeiten, einen SQL-Befehl an einen Server zu senden. Die universellste Möglichkeit ist hierbei die Methode query(). Dieser Methode können Sie direkt ein komplettes SQL-Statement übergeben: $erg = $db->query(’INSERT INTO plz(plz) VALUES(33602)’); Die Methode query() liefert Ihnen immer ein Objekt einer Statement-Klasse zurück. Diese sind für jeden Adapter einzeln deklariert. Als zweite Möglichkeit können Sie der Methode ein Statement übergeben, in dem noch Platzhalter in Form von Fragezeichen enthalten sind. Die dazugehöri- gen Werte werden dann als zweiter Parameter in Form eines Arrays übergeben. Hierbei ist allerdings zu beachten, dass nicht alle Datenbankadapter die Nutzung von Platzhaltern unterstützen. Welche Adapter damit umgehen können, können Sie Tabelle 2.3 entnehmen. Unterstützt der Adapter diese Vorgehensweise, so wäre das folgende Statement äquivalent zu dem vorhergehenden: $erg = $db->query(1INSERT INTO plz(plz) VALUES(?)1,array(33602)): Sollten in dem Statement mehrere Fragezeichen zu finden sein, so werden die Werte aus dem Array der Reihe nach zugeordnet. Damit aber nicht genug, es gibt noch eine dritte Möglichkeit. Und zwar können Sie auch benannte Platzhalter nutzen. Dies bedeutet, dass Sie als Platzhalter einen String nutzen, der mit einem Doppelpunkt eingeleitet wird. Auch hier werden die Werte wieder als Array übergeben. In diesem Fall handelt es sich allerdings um ein assoziatives Array, bei dem die Namen der Platzhalter, die im SQL-String verwendet wurden, als Schlüssel für die entsprechenden Werte genutzt werden. Sdaten = array (’:zahl' => 33602): $erg = $db->query('INSERT INTO plz(plz) VALUES(:zahl)',$daten); Angenehm an dieser Form der Wertübergabe an die Methode ist die Tatsache, dass sie sich in beiden Varianten automatisch um das Quoting kümmert, sodass Sie sich keine Gedanken darum machen müssen. 65
e-bol.net 2 | Datenbankzugriff mit Zend_Db Skunde = array (’: vorname ,=>,'Marcy", 1:nachname’=>"D'Arcy"): $erg = $db->query('INSERT INTO kundendaten (vorname, nachname) VALUES(:vornamenachname)’, $ künde); Auch hierbei gilt, dass nicht alle Datenbankadapter diese Notation unterstützen, wie Sie in Tabelle 2.3 sehen können. Adapter Fragezeichen Platzhalter Benannter Platzhalter AAysqli Ja Nein Oracle Nein Ja Db2 Ja Nein PDO-Adapter Ja Ja Tabelle 2.3 Unterstützung von Platzhaltern Die Variante, ein Statement an die Datenbank zu schicken, nutzt »Prepared State- ments«. Dabei wird die Abfrage zunächst vorbereitet, wobei auch hier Platzhalter verwendet werden können. Später können die Platzhalter durch konkrete Werte ersetzt und das Statement vom Server verarbeitet werden. Der Vorteil dieser Vor- gehensweise besteht darin, dass das eigentliche Statement bereits an den Server gesendet und von ihm analysiert wurde. Somit müssen immer nur die Werte aus- getauscht werden, wenn der Befehl ausgeführt wird. Falls Sie einen Befehl mehr- fach mit unterschiedlichen Werten ausführen wollen, ist eine solche Vorgehens- weise sehr hilfreich. Dadurch, dass nur die Werte übertragen werden müssen und der Befehl bereits analysiert ist, steigt die Performance deutlich. Die Methode queryO nutzt übrigens dieselbe Vorgehensweise und erledigt beide Schritte auf einmal. Um einen SQL-Befehl vorzubereiten, ist die Methode prepare() vorgesehen. Sie erhält den Befehl als String übergeben, wobei Sie sowohl das Fragezeichen als auch benannte Platzhalter nutzen können. Auch hierbei gilt natürlich, dass der entsprechende Datenbankadapter die Nutzung unterstützen muss. Der Rückga- bewert der Methode ist ein Statement-Objekt. Um den Befehl auszuführen, müs- sen Sie aus diesem Objekt heraus die Methode execute() aufrufen. Die Werte, die anstelle der Platzhalter eingefügt werden sollen, übergeben Sie der Methode als indiziertes oder als assoziatives Array. Die Verwendung könnte also beispiels- weise wie folgt aussehen: // Daten die eingefügt werden sollen Skunden = array ( array("Homer", "Simpson"), array("Spider", "Pig")); // Statement vorbereiten 66
e-bol.net Nutzung von Zend_Db | 2.2 Squery = $db->prepare('INSERT INTO kundendaten (vorname, nachname) VALUES(?.?)’); // Datensätze an den Server senden foreach ($kunden as $kunde) { $query->execute($ künde); } Listing 2.5 Ausführen von Prepared Statements Eine zweite Möglichkeit, um mit Prepared Statements zu arbeiten, stellt die Bin- dung von Parametern an Variablen dar. In diesem Fall werden die Werte nicht direkt an executel) übergeben. Vielmehr wird ein Platzhalter mit einer Variab- len oder einem Wert verbunden. Sobald die Methode executeO aufgerufen wird, wird der Wert der Variablen bzw. der zugewiesene Wert in das Statement übernommen: //Variablen initialisieren Svorname = nul1 : Snachname = nul1; // Statement vorbereiten Ssql = 'INSERT INTO kundendaten (vorname, nachname) VALUES(?,?)': Squery = $db->prepare(Ssql); // Parameter an Variablen binden Squery->bi ndParam(1,$vorname): Squery->bindParam(2,$nachname): // Werte zuweisen Svorname = 'Marge': Snachname = 'Simpson'; // Statement ausführen $query->execute(); Listing 2.6 Prepared Statement mit Bind-Variablen Wie Sie hier sehen, bekommt die Methode bi ndParam() zwei Parameter überge- ben. Der erste bezeichnet den Parameter. Bei einem benannten Parameter wird hier also der Name übergeben. Falls Sie Fragezeichen nutzen, wird die Ordnungs- nummer genutzt, wobei der erste Parameter die Nummer 1 hat. Wie schon ange- deutet, können Sie auch Konstanten an Parameter binden, wobei das wohl eher selten vorkommen wird. Dafür ist die Methode bi ndVal ue() deklariert. Auch Sie bekommt als ersten Parameter die Information übergeben, um welchen Platzhal- ter es sich handelt, und als zweiten die Konstante, durch die der Platzhalter ersetzt werden soll. 67
e-bol.net 2 | Datenbankzugriff mit Zend_Db Bei den bisher vorgestellten Möglichkeiten, ein Statement zum Server zu senden, habe ich immer auf Befehle zurückgegriffen, die kein Datenbankergebnis liefern. Dennoch geben sie, wie schon erwähnt, ein Statement-Objekt zurück. Hiermit können Sie beispielsweise die Anzahl der Zeilen, die vom letzten Befehl betroffen waren, über die Methode rowCountl) auslesen. Sollte bei einem der Befehle ein Fehler auftreten, wird eine Exception geworfen. Was aber passiert, wenn Sie ein SELECT ausführen, das ja in den meisten Fällen ein Ergebnis liefert? Zunächst gilt, dass das Ergebnis der Abfrage mit im State- ment-Objekt gespeichert wird. Das heißt, Sie erhalten genau dieselbe Art von Ergebnis-Objekt, wie Sie es bereits kennengelernt haben. Um die Daten aus dem Statement-Objekt auszulesen, stehen relativ viele Metho- den zur Verfügung. Am einfachsten wird sicherlich die Methode fetchO zu handhaben sein. Sie liefert immer eine Zeile aus der Ergebnismenge zurück bzw. NULL, falls keine Daten mehr gelesen werden können. Somit eignet sich die Methode ganz wunderbar für den Einsatz in einer whi 1 e-Schleife, wie Sie in Lis- ting Listing 2.7 sehen können: require_once ' Zend/Db.php': Soptionen = array( 'host’ => '127.0.0.1’, 'username’ => 'root', 'password’ => '', ’dbname' => 'test' ); $db = Zend_Db::factory(’mysqli',$optionen); // Statement vorbereiten $sql = 'SELECT vorname, nachname FROM kundendaten LIMIT 0,3’: // Query ausführen $res = $db->query($sql): // Daten ausgeben while (Szeile = $res->fetch()) { echo ’<p>': echo 'Vorname: '.$zei1e['vorname'].'<br>' ; echo 'Nachname: '.$zei1e['nachname<br>'; echo '</p>': } Listing 2.7 Auslesen von Daten mit fetchO Die Ausgabe des Listings sehen Sie in Abbildung 2.1. 68
e-bol.net Nutzung von Zend_Db | 2.2 httpi//.. Db/l.php CD Vorname: Marge Nachname: Simpson Vorname: Homer Nachname: Simpson Vorname: Monty Nachname: Bums 4 Abbildung 2.1 Ausgabe von Daten mit fetch() Wie Sie sehen, werden die Daten per Default als assoziatives Array zurückgege- ben. Dies können Sie allerdings über den Fetch-Mode beeinflussen. Diesen kön- nen Sie entweder über die Methode setFetchMode() festlegen, oder Sie überge- ben der Methode fetch() eine Konstante, die definiert, auf welche Art Sie die Daten erhalten möchten. So könnten Sie mit $db->setFetchMode(Zend_Db::FETCH_OBJ); definieren, dass Sie die Daten als ein Objekt der Klasse stdCl ass erhalten möch- ten. Den gleichen Effekt erzielen Sie, wenn Sie die Konstante direkt an fetch() übergeben: $res->fetch(Zend_Db::FETCH_OBJ) Neben FETCH_OBJ sind noch eine Reihe anderer Kostanten deklariert. Mit FETCH_ ASSOC können Sie explizit festlegen, dass Sie die Daten als assoziatives Array erhalten möchten, was ja auch der Default-Wert ist. Diese Konstante kann hilf- reich sein, wenn Sie mit setFetchModel) den Modus global umgeschaltet haben und nur bei einem fetchO ein assoziatives Array auslesen wollen. Nutzen Sie den Modus FETCH_NUM, so erhalten Sie die Daten als indiziertes Array zurück. Sollte es notwendig sein, können Sie auch FETCH_BOTH nutzen. In diesem Fall erhalten Sie ein Array, bei dem die Daten über den Index und über den Spalten- namen ansprechbar sind. Bezogen auf die Abfrage aus dem vorhergehenden Lis- ting könnte ein solches Array wie folgt aufgebaut sein: array(4) { [0]=> string(5) "Marge" [1]=> string(7) "Simpson" 69
e-bol.net 2 | Datenbankzugriff mit Zend_Db ["vorname"]=> string(5) "Marge" ["nachname"]=> string(7) "Simpson" } Allerdings sind das noch nicht alle Möglichkeiten. Möchten Sie sämtliche Daten einer Abfrage in einem Array übergeben bekommen, so steht die Methode fetchAl1 () zur Verfügung. Genau genommen müsste man sagen die »Metho- den« fetchAl 1 (), da zwei Methoden mit diesem Namen existieren. Die erste ist in den Statement-Klassen deklariert. Das heißt, Sie können die Abfrage zuerst mit der Methode query() zur Datenbank schicken und dann alle Daten auf einmal mit fetchAl 1 () auslesen, wobei Sie auch dabei einen Fetch-Mode übergeben können. Die zweite Möglichkeit besteht darin, die Methode fetchAl 1 () aus dem eigentlichen Datenbankobjekt heraus aufzurufen. Dabei übergeben Sie als ersten Parameter das SQL-Statement, welches Platzhalter enthalten darf. Sollten Platz- halter enthalten sein, so müssen Sie die entsprechenden Werte als zweiten Para- meter übergeben. Der Fetch-Mode kann dabei nicht direkt gesetzt werden, son- dern muss vorher mithilfe von setFetchModel) festgelegt werden. Somit wären die folgenden Code-Fragmente äquivalent: // Erste Möglichkeit Ssql = 'SELECT vorname, nachname FROM kundendaten LIMIT 0,3’: Sres = $db->query(Ssql): Sdaten = Sres->fetchAl 1 (Zend_Db::FETCH_NUM); // Zweite Möglichkeit Sdb->setFetchMode(Zend_Db::FETCH_NUM); Ssql = 'SELECT vorname, nachname FROM kundendaten LIMIT 0,3’: Sdaten = $db->fetchAl 1($sql): Ein wenig unflexibler, aber oft völlig ausreichend ist die Methode f etchAssoc(). Diese Member-Funktion führt eine Datenbankabfrage aus und gibt die Daten als Array zurück. In der ersten Ebene ist dieses indiziert, und in der zweiten Ebene handelt es sich um ein assoziatives Array. Als ersten Parameter bekommt die Methode ein SELECT-Statement übergeben, welches auch Platzhalter enthalten darf. Die dazugehörigen Werte können Sie dann als zweiten Parameter überge- ben. Im Bedarfsfall werden diese automatisch escapet. Ssql = 'SELECT vorname, nachname FROM kundendaten LIMIT 0,?’: Sdaten = Sdb->fetchAssoc(Ssql,3): Sollten Sie es bevorzugen, das Ergebnis einer Abfrage als Objekt auszulesen, ist dies ebenfalls kein Problem. Die Methode fetchObject(), die die Daten für Sie 70
e-bol.net Nutzung von Zend_Db | 2.2 auslesen kann, ist allerdings nur für das Ergebnis einer Abfrage definiert. Das bedeutet, dass Sie diese zuvor mit query() ausführen müssen. Sie können dann jeweils eine Zeile aus dem Ergebnis mit fetchObject!) auslesen: $sql = 'SELECT vorname, nachname FROM kundendaten LIMIT 0,3’: $erg = $db->query($sql); while($daten= $erg->fetchObject()) { echo "Vorname: ".$daten->vorname: // Weitere Verarbeitung } Wie in PHP üblich, wird hier standardmäßig die Klasse stdClass genutzt. Die Spaltennamen werden dabei gleichzeitig als Namen für die Eigenschaften ver- wendet. Sehr praktisch ist, dass Sie nicht darauf festgelegt sind. Sie können der Methode zwei optionale Parameter übergeben. Mit dem ersten definieren Sie den Namen der Klasse, die genutzt werden soll. Ein bereits abgeleitetes Objekt zu übergeben, ist leider nicht möglich. Mit dem zweiten Parameter können Sie ein Array mit Daten übergeben. Dieses Array, also tatsächlich das komplette Array und nicht die einzelnen Werte, wird dem Konstruktor der Klasse als Parameter übergeben. Möchten Sie nur eine Spalte aus einer Abfrage erhalten, so steht die Methode fetchColO zur Verfügung. Auch sie bekommt direkt die Datenbankabfrage übergeben und kann dabei auch mit Platzhaltern umgehen. Als Rückgabewert erhalten Sie ein indiziertes Array mit allen Werten, die in der ersten Spalte des Ergebnisses enthalten waren. Das mag sich zuerst ein wenig kurios anhören. Sofern Sie aber nur eine Datenbankspalte auslesen wollen, ist die Methode durchaus praktisch, da sie Ihnen die Verarbeitung eines mehrdimensionalen Arrays erspart. Bevorzugen Sie es, Ihre Abfrage mit query() zu versenden, so können Sie die Daten dennoch spaltenweise auslesen. Dafür ist die Methode fetchCol umnO gedacht. Sie leistet im Endeffekt das Gleiche wie fetchColO, wird allerdings direkt auf das Ergebnis einer Abfrage angewandt. Ein kleiner Unterschied besteht aber. fetchCol umn() liefert jeweils immer nur einen einzelnen Wert zurück und kein Array, wie dies bei fetchCol () der Fall ist. Bitte verwechseln Sie die beiden Methoden nicht! Die beiden folgenden Beispiele leisten also das Gleiche: //Variante 1: Nutzung von fetchCol!) $sql = 'SELECT vorname, nachname FROM kundendaten LIMIT 0,3’: Sdaten = $db->fetchCol($sql): foreach!$daten as $vorname) ( 71
e-bol.net 2 | Datenbankzugriff mit Zend_Db echo "Vorname: ".Svorname."<br>"; } //Variante 2: Nutzung von fetchColumn() $sql = ’SELECT vorname, nachname FROM kundendaten LIMIT 0,3’: $erg = $db->query($sql): whi1e($vorname = $erg->fetchColumn()) { echo "Vorname: ".Svorname."<br>"; } Listing 2.8 Ausgeben von Spalten Eine ähnliche Idee wie beim Auslesen der Spalten liegt der Methode fetchRowl) zugrunde. Allerdings liefert sie nicht die erste Spalte, sondern die Zeile des Ergebnisses zurück, wobei auch sie das SQL-Statement als Parameter übergeben bekommt. Sie beachtet übrigens den voreingestellten Fetch-Mode, sodass Sie die Daten beispielsweise auch als Objekt auslesen können. Wenn Sie nur eine Zeile auslesen, kann diese Methode Ihnen Arbeit abnehmen. Falls Sie nur einen einzelnen Wert aus einer Datenbank auslesen wollen, ist fetchOnel) Ihr Kandidat. Diese Funktion liefert den Wert, der sich als erster in der ersten Spalte befindet. Im Endeffekt ist diese Methode also wirklich nur dann hilfreich, wenn Sie nur exakt einen Wert aus der Datenbank auslesen wollen. Dieser eine Wert wird dann direkt als skalare Variable und nicht in Form eines Arrays zurückgegeben. Eine Methode, die auch ungemein praktisch sein kann, ist fetchPai rs(). Sie bekommt ebenfalls ein SELECT-Statement übergeben. Die Methode verarbeitet nur die ersten beiden Spalten der Ergebnismenge und gibt sie als Array zurück. Dabei nutzt sie die Werte der ersten Spalte als Schlüssel und die der zweiten Spalte als Werte. Das ist sehr praktisch, wenn Sie beispielsweise den Primär- schlüssel und einen zusätzlichen Wert aus der Datenbank auslesen wollen. So könnte eine Abfrage wie die folgende $sql = ’SELECT id, nachname FROM kundendaten LIMIT 0,3': Sdaten = $db->fetchPai rs($sq1): ein Ergebnis liefern, das folgendermaßen aufgebaut ist: array(3) { E27» string(7) "Simpson" E28» string(7) "Simpson" 72
e-bol.net Nutzung von Zend_Db | 2.2 [29» string(5) "Burns" } Hier verweist der Wert aus der Spalte i d auf den dazugehörigen Wert aus der Spalte nachname. 2.2.1 Transaktionen Zend_Db gibt Ihnen auch die Möglichkeit, transaktionsorientiert zu arbeiten. Transaktionsorientierung bedeutet, dass Sie die Möglichkeit haben, ein »Undo« der letzen Datenbankaktionen auszuführen. Dies kann sehr praktisch sein, falls Sie beispielsweise mehrere Tabellen aktualisieren müssen und die Daten vonein- ander abhängig sind. Führen Sie die notwendigen Befehle im Rahmen einer Transaktion aus, so können Sie jederzeit alle Befehle rückgängig machen, falls einer der Befehle fehlschlägt. Das setzt allerdings voraus, dass Sie eine Datenbank nutzen, die transaktionsfähig ist. Sollten Sie MySQL verwenden, ist das kein Problem. Allerdings müssen Sie in diesem Fall InnoDB-Tabellen nutzen. Die üblichen MylSAM-Tabellen unterstüt- zen leider keine Transaktionen. Für das folgende Beispiel wurde diese MySQL-Tabelle genutzt: CREATE TABLE 'telefonnummern' ( 'id' int(ll) NOT NULL AUTO_INCREMENT, 'name' varchar(lOO) DEFAULT NULL, 'telefon' varchar(lOO) DEFAULT NULL, PRIMARY KEY ('id' ) ) ENGINE=InnoDB DEFAULT CHARSET=latinl Eine Transaktion wird mit beginTransaction() eingeleitet. Alle Befehle, die danach kommen, werden erst temporär in einem Bereich ausgeführt, auf den nur der aktuelle Benutzer Zugriff hat. Sie können von dem Benutzer auch schon selektiert werden. Wurden alle Befehle korrekt ausgeführt, können Sie die Trans- aktion mit commitO bestätigen. Alle Daten werden daraufhin in den globalen Kontext übernommen, sodass jeder Benutzer die Änderungen sieht. Wollen Sie die Auswirkungen der Befehle verwerfen, so rufen Sie die Methode rol l Back() auf, die alle Änderungen, Lösch- und Einfügeoperationen der Transaktion ver- wirft. Das folgende Listing veranschaulicht das Verhalten: require_once ’Zend/Db.php’; // Liest alle Daten aus der Tabelle und gibt sie aus function datenAuslesen(Sdb) 73
e-bol.net 2 | Datenbankzugriff mit Zend_Db $sql = 'SELECT * FROM telefonnummern'; // Query ausführen $res = $db->query($sql); // Daten ausgeben echo ’<table>'; while ($zeile = $res->fetch()) { echo '<tr>'; echo ’<td>ID: '.$zei1e[’id'1.'</td>’; echo ’<td>Name: ’.$zei1e['name'].'</td>’; echo '<td>Telefon: '.$zei1e['telefon'].'</td>'; echo ' </tr>'; echo '</table>'; Soptionen = array( 'host’ => '127.0.0.1', 'username’ => 'root', 'password’ => '', ’dbname' => 'Warenwirtschaft' ); $db = Zend_Db::factory(’mysqli',$optionen); echo 'Daten vorher:<br>'; datenAuslesen($db): // Normales INSERT ohne Transaktion $sql = 'INSERT INTO telefonnummern (name, telefon) VALUES ("Monty Burns", "+1 1343 1111");'; $db->query($sql); // Transaktion beginnen $db->begi nTransaction(); // INSERT in einer Transaktion $sql = 'INSERT INTO telefonnummern (name, telefon) VALUES ("Patrick Star", "+1 312 343431947");'; $db->query($sql); echo ’CbODaten vor Rol 1 back:<br>'; datenAuslesen($db); // Daten verwerfen $db->rol1Back(); // Neue Transaktion $db->begi nT ransaction(); 74
e-bol.net Nutzung von Zend_Db | 2.2 $sql = 'INSERT INTO telefonnummern (name. telefon) VALUES ("Eugene Krabs", "+1 312 615235398");’: $db->query($sq1): // Transaktion bestätigen $db->commi t(); echo ’CbODaten nachher:<br>’; datenAuslesen($db); Listing 2.9 Nutzung von Transaktionen In diesem Listing werden drei INSERT-Anweisungen ausgeführt: Die erste kom- plett ohne eine Transaktion. Die zweite INSERT-Anweisung innerhalb einer Transaktion, die mit rol 1 Back() verworfen wird, und die letzte INSERT-Anwei- sung befindet sich ebenfalls in einer Transaktion, wobei dieses Statement aller- dings mit commit() bestätigt wird. Die generierte Ausgabe sehen Sie in Abbil- dung 2.2. http//127.0.0.1/~carsten/zf-buch/Db/5.php (£ W & Ü (Ü Google > |Uj Daten vorher ID: 1 Name: Homer Simpson Telefon: +1 1343 2232222 Daten vor Rollback: ID: 1 Name: Homer Simpson Telefon: +1 1343 2232222 ID: 20 Name: Monty Bums Telefon: +1 1343 1111 ID: 21 Name: Patrick Star Telefon: +1 312 343431947 Daten nachher ID: 1 Name: Homer Simpson Telefon: +1 1343 2232222 ID: 20 Name: Monty Bums Telefon: +1 1343 1111 ID: 22 Name: Eugene Krabs Telefon: +1 312 615235398 Abbildung 2.2 Nutzung von Transaktionen 2.2.2 Sequenzen und automatisch generierte IDs In vielen Fällen werden Sie nach dem Einfügen eines Datensatzes wissen wollen, welche ID ihm von dem Datenbanksystem zugewiesen wurde. Wie schon erwähnt, gibt es hier verschiedene Vorgehensweisen, wie die Datenbanksysteme arbeiten. Zunächst ist aber in allen Adaptern die Methode lastlnsertldO defi- niert, die Ihnen die letzte ID liefert, die genutzt wurde. Da die Datenbanken sich in ihren Möglichkeiten unterscheiden, ist diese Methode auch deutlich unter- schiedlich implementiert. Die MySQL-Adapter liefern beispielsweise einfach die 75
e-bol.net 2 | Datenbankzugriff mit Zend_Db zuletzt vergebene ID einer Auto-Inkrement-Spalte, wohingegen die Oracle-Adap- ter auch auf Sequenzen zugreifen können. Das heißt, die Methode akzeptiert zwei Parameter, die aber bei Nutzung von MySQL ignoriert werden. Der erste ist der Name der Tabelle und der zweite der Name der Spalte, die den Primärschlüs- sel darstellt. Rufen Sie die Methode mit den Parametern 'kundendaten' und ’id_kunde' auf, so liest die Methode den aktuellen Wert der Sequenz kundendaten_i d_kunde_seq aus. Übergeben Sie den Namen des Primärschlüssels nicht, greift die Methode auf die Sequenz kündendaten_seq zu. Rufen Sie die Methode ohne Parameter auf, liefert sie Ihnen die zuletzt eingefügte ID zurück. Sollten Sie Sequenzen nutzen, die nach einem anderen Namensschema benannt sind, können Ihnen die Methoden 1 astSequenceldf) und nextSequenceldf) helfen. Die beiden Methoden bekommen den Namen der Sequenz übergeben und liefern Ihnen den letzten bzw. den nächsten Wert der Sequenz zurück. Bitte beachten Sie hierbei, dass die Methoden nur dann definiert sind, sofern die ent- sprechende Datenbank Sequenzen unterstützt. 2.2.3 Spezielle Datenbankzugriffsmethoden Die bereits erläuterten Methoden sind eher allgemeiner Natur bzw. beziehen sich primär auf die Verarbeitung eines SELECT-Statements, wobei die Methode query () universell einsetzbar ist. Zend_Db kennt allerdings noch eine ganze Reihe von Methoden, die auf bestimmte SQL-Befehle spezialisiert sind. Mithilfe dieser Befehle können Sie ein SQL-Statement generieren, ohne sich um die Syntax der Befehle kümmern zu müssen. Das hat natürlich zur Folge, dass Sie nicht ganz so flexibel sind, als würden Sie den Befehl von Hand erstellen. Auch wenn es darum geht, einen möglichst performanten Befehl zu erstellen, ist es meiner Ansicht nach sinnvoll, den Befehl manuell zu erstellen und zu optimieren. Trotzdem kann es hilfreich sein, die SQL-Statements maschinell generieren zu lassen. Um einen neuen Befehl zu generieren, stehen innerhalb des Datenbankobjekts entsprechende Methoden zur Verfügung. Wenn Sie also einen SELECT-Befehl erstellen wollen, so rufen Sie die Methode selectt) auf, und bei einem INSERT- Befehl nutzen Sie dementsprechend i nsert(). Daten einfügen Um Daten in eine Tabelle einzufügen, können Sie die Methode insert() aufru- fen. Sie bekommt als ersten Parameter den Namen der Tabelle übergeben. Der zweite Parameter ist das Array mit denjenigen Daten, die in die Tabelle eingefügt werden sollen. Dabei wird der Spaltenname als Schlüssel genutzt und der dazu- gehörige Array-Wert wird in das Statement als Einfügewert übernommen. Dabei 76
e-bol.net Nutzung von Zend_Db | 2.2 müssen Sie sich auch nicht um das Quoting der Werte kümmern. Das übernimmt die Methode für Sie. Um das folgende SQL-Statement INSERT INTO 'kundendaten' ('vorname', 'nachname') VALUES ('Patrick', 'Star'); zu generieren und auszuführen, würde der Methodenaufruf etwa so aussehen: $db = Zend_Db::factory(’mysqli',$optionen); Sdaten = array('vorname'=>'Patrick', 'nachname'=>'Star'); $erg = $db->insert('kundendaten',$daten); echo "Es wurden $erg Zeilen eingefügt"; Listing 2.10 Einfügen von Daten mithilfe von insertO Der Rückgabewert enthält, wie Sie nun vielleicht schon erahnen, die Anzahl der eingefügten Zeilen. Da die Methode aber keine Möglichkeit vorsieht, mehr als eine Zeile einzufügen, wird üblicherweise die Zahl 1 zurückgeliefert. Löschen von Daten Auf diesem Weg können Sie auch recht einfach einen Datensatz löschen. Das übernimmt die Methode del ete() für Sie. Sie bekommt als ersten Parameter den Namen der Tabelle übergeben. Bei dem zweiten Parameter handelt es sich um die Bedingung, die in der WHERE-Bedingung genutzt werden soll. Diese wird direkt als String übergeben. Falls Sie den Datensatz, der im letzten Beispiel eingefügt wurde, wieder entfernen wollen, würde der Aufruf folgendermaßen lauten: $db = Zend_Db::factory(’mysqli',$optionen); $erg = $db->delete('kundendaten',"vorname=’Patrick' AND nachname='Star'"); echo "Es wurden $erg Datensätze gelöscht"; Listing 2.11 Löschen von Datenbankeinträgen mithilfe von deleteO Auch hier wird die Anzahl der betroffenen bzw. gelöschten Zeilen zurückgege- ben. Bitte stellen Sie aufjeden Fall sicher, dass der zweite Parameter nicht leer ist. Sollte hier eine leere Variable oder ein leerer String übergeben werden, wird kein WHERE an das DELETE-Statement angefügt und der gesamte Inhalt der Tabelle gelöscht. Daten aktualisieren Natürlich gibt es auch eine Funktion, mit der Sie Daten aktualisieren können. Wie Sie nun sicher schon annehmen, lautet der Name der Funktion update(). Auch sie bekommt als ersten Parameter den Namen der Tabelle übergeben. Beim 77
e-bol.net 2 | Datenbankzugriff mit Zend_Db zweiten Parameter handelt es sich wiederum um ein Array, bei dem die Namen der Schlüssel als Spaltennamen genutzt werden. Die Werte werden dementspre- chend in die Spalten übernommen. Um dem System mitzuteilen, welcher Daten- satz aktualisiert werden soll, geben Sie nach dem Array noch einen String an, der die Bedingung für die WHERE-Klausel enthält. Auch hierbei gilt, dass Sie unbedingt darauf achten sollten, keinen Leerstring zu übergeben. Andernfalls wird das UPDATE-Statement auf alle Zeilen angewandt. Ein Aufruf der Methode könnte so aussehen: Sdaten = array (’vonname’=>’Spongebob', ’nachname'=>'Squanepants'): $eng = $db->update('kundendaten', Sdaten, *id=33’); echo "Es wurden $erg Datensätze aktualisiert": Listing 2.12 Aktualisieren von Daten mithilfe von updateO Daten selektieren Vielleicht haben Sie schon nach einer Möglichkeit gesucht, um ein SELECT-State- ment generieren zu lassen. Natürlich besteht auch diese Möglichkeit. Allerdings bietet ein SELECT-Statement in SQL deutlich mehr Features als die anderen Befehle. Dieser Problematik trägt das Zend Framework Rechnung. Daraus resul- tiert eine etwas andere Vorgehensweise als bei den übrigen Befehlen. Rufen Sie die zuständige Methode selectt) auf, wird nicht direkt ein Befehl ausgeführt. Daher akzeptiert sie auch keine Parameter. Vielmehr stellt sie eine Art Factory dar und generiert ein Objekt der Klasse Zend_Db_Select, die auf SELECT-State- ments spezialisiert ist. Die Klasse kennt eine Vielzahl von Methoden, die Sie dabei unterstützen, einen SELECT-Befehl zu generieren. Auch diese Klasse unterstützt ein fluent-Interface, sodass Methoden direkt hin- tereinander aufgerufen werden können, was in diesem Fall die Lesbarkeit verbes- sern dürfte. Wie schon erwähnt, können Sie die Methode selectt) direkt aus dem Daten- bankobjekt heraus aufrufen, um ein neues SELECT-Objekt zu erhalten. Das so generierte Objekt benötigt natürlich noch einige Informationen von Ihnen. Da ist zunächst die Frage, was von woher selektiert werden soll. Diese Daten legen Sie mit der Methode from() fest. Sie erhält als ersten Parameter den Namen der Tabelle. Wollen Sie die Tabelle komplett auslesen, können Sie es dabei bewenden lassen. Mit den folgenden Zeilen würde die komplette Tabelle kundendaten aus- gelesen: Sselect = $db->select(): $select->from('kundendaten'); Seng = $db->fetchAH($select): 78
e-bol.net Nutzung von Zend_Db | 2.2 Nachdem Sie das SELECT-Objekt korrekt konfiguriert haben, können Sie es an eine Methode übergeben, welche die Abfrage ausführt. Eine der Methoden, die Sie dafür nutzen können, ist fetchAl 1(). Sie ist, genau wie die Methoden fetchAssoc(), fetchCol(), fetchPai rs(), fetchRowi) und fetchOne(), in der Lage, ein solches Objekt zu verarbeiten. Alternativ können Sie auch aus dem SELECT-Objekt die Methode query() aufru- fen, welche dann das Ergebnis in derselben Form zurückgibt wie die Methode query() des Datenbankobjekts. Gerade bei umfangreichen SELECT-Statements kann es interessant sein, das State- ment ausgeben zu lassen. Das können Sie beispielsweise dadurch erreichen, dass Sie das SELECT-Objekt direkt ausgeben lassen oder es explizit in einen String kon- vertieren: // Direkte Ausgabe des Statements echo $select: // Statement in Variable speichern Sstatement = (string) $select: Natürlich können Sie auch angeben, welche Spalten Sie selektieren wollen. Dazu geben Sie nach dem Namen der Tabelle einfach den Namen der Spalte als String an. Sollte es sich um mehrere Spalten handeln, so übergeben Sie die Namen als Array. Als dritten Parameter können Sie noch ein Schema, also beispielsweise den Namen der Datenbank, übergeben. Um aus der Tabelle kundendaten, die sich in der Datenbank warenwi rtschaft befindet, die Spalten vorname und nach- name auszulesen, würde der entsprechende Befehl so lauten: Sspalten = array('vorname', 'nachname'): Sdatenbank = 'Warenwirtschaft': Sselect = $db->select(): $select->from('kundendaten', tspalten, $datenbank); Listing 2.13 Generieren eines Statements Der SQL-Befehl, der in diesem Fall genutzt wird, lautet wie folgt: SELECT 'kundendaten'.'vorname', 'kundendaten'.'nachname’ FROM 'Warenwirtschaft'.'kundendaten’ Wie Sie sehen, werden die Spalten-, Tabellen- und Schemanamen automatisch gequotet. In vielen Fällen werden Sie die den Tabellennamen bzw. einen Spaltennamen durch ein Alias ersetzen wollen. Oder Sie möchten nicht direkt das Ergebnis einer Spalte auslesen, sondern eine Aggregat-Funktion auf die Spalte anwenden. 79
e-bol.net 2 | Datenbankzugriff mit Zend_Db Auch das ist alles möglich. In diesen Fällen übergeben Sie die Daten in einem Array, bei dem der Schlüssel dem Parameter entspricht. Der Wert wird dann direkt in die Abfrage übernommen. Diese Technik funktioniert sowohl bei den Tabellennamen als auch bei den Spaltennamen. Möchten Sie beispielsweise die Anzahl der IDs ermitteln, so können Sie in der Abfrage die Funktion CDU NT () nutzen. Das Generieren einer entsprechenden Abfrage könnte beispielsweise so aussehen: Stabelle = array ('k' => 'kundendaten'); Sspalte = array ('anzahl’ => 'COUNT(k.id)'); Sselect = $db->select(): Sselect->from(Stabei le, Sspalte); Das Statement, das hier generiert wird, lautet: SELECT COUNT(k.id) AS 'anzahl' FROM 'kundendaten' AS 'k' Der Tabellenname wurde hier durch ein Alias ersetzt, das dann beim Zugriff auf die Spalten genutzt wurde. Die Tabellennamen mit einem Alias zu ersetzen, ist auch bei einem Self-Join sehr hilfreich, wie Sie später noch sehen werden. Nutzen von WHERE-Klauseln Natürlich haben die meisten SELECT-Befehle wenig Sinn, wenn Sie nicht noch die eine oder andere WHERE-Klausel nutzen können. Um ein WHERE zu ergänzen, über- geben Sie die Bedingung an die Methode where(). Sollten Sie mehrere Bedingun- gen hinzufügen wollen, können Sie die Methode mehrfach aufrufen. Die einzel- nen Bestandteile der Bedingung werden dann jeweils mit einem AND verknüpft. Sollten Sie eine OR-Verknüpfung benötigen, so übergeben Sie die Bedingung an die Methode orWhere(): Sselect = $db->select(): Sselect->from(’kontaktdaten'nachname’): Sselect->where('id = 12'): Sselect->orWhere(' id = 20'): Mit diesen Zeilen wird das folgende SQL-Statement generiert: SELECT 'kontaktdaten'.'nachname' FROM 'kontaktdaten' WHERE (id = 12) OR (id = 20) Was aber, wenn Sie eine WHERE-Bedingung benötigen wie ((id = 10) OR (id = 20)) AND (vorname = 'Marge')? Da Sie die Klammern nicht manuell setzen kön- nen, müssen Sie die beiden Teile, die durch AND verbunden werden, mit der Methode where() setzen: 8o
e-bol.net Nutzung von Zend_Db | 2.2 $select->from('kontaktdaten'nachname'); $select->where('(id = 10) OR (id = 20)'); $select->where('vorname = ?’, 'Marge'); Wie Sie sehen, wurde bei dem zweiten Aufruf von where() mit einem Platzhalter gearbeitet. Der Wert wird danach als Parameter angegeben. Wenn er übernom- men wird, quotet das System ihn automatisch. Bei dem ersten Aufruf war diese Vorgehensweise nicht möglich, da where() immer nur einen Wert für die Platz- halter akzeptiert. Mehrere Werte zu übergeben, ist daher zurzeit leider nicht möglich. Allerdings können Sie mehrere Platzhalter nutzen und alle mit dem sel- ben Wert belegen. Die Nutzung benannter Platzhalter ist bei diesen Methoden momentan1 auch noch nicht möglich. Ich kann mir aber vorstellen, dass der Funktionsumfang dieser Methoden noch erweitert wird. Daher sollten Sie einen Blick in die Dokumentation werfen, sofern Sie die das Paket nutzen möchten. Nutzung von Limits Möchten Sie seitenweise durch ein Abfrageergebnis blättern, ist es hilfreich, wenn Sie nur eine bestimmte Anzahl von Werten pro Abfrage erhalten. Wie viele Werte Sie erhalten möchten, können Sie dabei mit der Methode 1 imi t() festle- gen. Sie bekommt als ersten Parameter die Anzahl der Zeilen, die ausgelesen wer- den sollen, übergeben. Der zweite Parameter definiert den Offset, das heißt, wie viele Zeilen ausgehend von der ersten Zeile der Ergebnismenge ausgelesen wer- den sollen. Um die Daten gleich ab der ersten Zeile zu erhalten, lassen Sie diesen Parameter einfach wegfallen, da er optional ist. Mit der Anweisung $select->from('kundendaten', array('vorname','nachname')); $select->limit(5,10); würde ein Statement generiert, das Ihnen fünf Zeilen zurückgibt, wobei die ers- ten zehn Zeilen übersprungen werden. Falls Sie seitenweise blättern wollen, kann es ein aufwändig sein mitzurechnen, welcher Offset zu nutzen ist. Um Ihnen das Leben an dieser Stelle ein wenig zu erleichtern, ist die Methode 1 i mi t - PageO definiert. Sie bekommt als ersten Parameter die Nummer der »Seite« übergeben, von der Sie die Daten auslesen wollen. Der zweite Parameter ist die Anzahl der Zeilen, die eine Seite umfassen soll. Diese Zeilen generieren also die- selbe Ergebnismenge wie das vorhergehende Beispiel: $select->from('kundendaten', array('vorname','nachname')); $seiect->11 mitPage(3,5); 1 Bezieht sich auf Version 1.0.3. 81
e-bol.net 2 | Datenbankzugriff mit Zend_Db Damit erhalten Sie also die dritte »Seite« aus dem Ergebnis, wobei jede Seite fünf Zeilen umfasst. Anders formuliert: Das Ergebnis umfasst die Zeilen 11 bis 15, somit werden die Zeilen 1 bis 10 übersprungen. Ein Ergebnis sortieren Um die Sortierreihenfolge für ein Ergebnis festzulegen, können Sie die Methode ordert) nutzen. Ihr können Sie den Namen einer Spalte als String oder die Namen mehrerer Spalten als Array übergeben. Sie können nach dem Namen der Spalte jeweils noch ASC oder DESC angeben, um die Sortierrichtung festzulegen. Falls Sie nichts angeben, ergänzt die Methode automatisch ASC, um eine aufstei- gende Sortierung zu erzielen. Wollen Sie beispielsweise die Namen Ihrer Kunden auslesen und die Nachnamen dabei absteigend sortieren, können Sie das auf fol- gende Weise lösen: $select->from('kundendaten', arrayt'vorname','nachname')); $select->order(array('nachname DESC,'vorname')): Bei gleichen Nachnamen würde in diesem Beispiel aufsteigend nach Vornamen sortiert, da die Methode automatisch ASC ergänzt. GROUP BY und HAVING Selbstverständlich unterstützt die Klasse auch die Möglichkeit, mit GROUP BY und HAVING zu arbeiten. Um Daten nach einer Spalte zu gruppieren, übergeben Sie den Spaltennamen zusammen mit der Methode group(). Wenn Sie nach mehre- ren Spalten gruppieren wollen, können Sie die Namen der einzelnen Spalten auch als Array an die Methode weiterleiten. Benötigen Sie zusätzlich eine HAVING-Klausel, so übergeben Sie die gewünschte Bedingung als String an die Methode having(). Hierbei können Sie auch mit einem Platzhalter in der Bedingung arbeiten und den dazugehörigen Wert als zweiten Parameter übergeben. Also beispielsweise so etwas wie havi ng (' Umsatz > ? ’, 30000). Auch hier gilt, dass dies nur mit einem Parameter möglich ist. Falls Sie mehrere Bedingungen bei der HAVING-Klausel benötigen, können Sie die Methode mehrfach aufrufen. Die einzelnen Teile werden dann mit AND ver- knüpft. Sollten Sie eine Verknüpfung mit 0R benötigen, können Sie auf die Methode orHaving() zurückgreifen, welche die übergebene Bedingung mit 0R anhängt, ansonsten aber identisch mit havi ng() ist. DISTINCT und FOR UPDATE Neben den schon erläuterten Funktionalitäten kennt ein SELECT-Statement noch mehr Features. Wollen Sie verhindern, dass Ergebnisse doppelt vorkommen, so 82
e-bol.net Nutzung von Zend_Db | 2.2 ist das Schlüsselwort DI ST INCT hilfreich. Dieses können Sie der Abfrage dadurch hinzufügen, dass Sie die Methode di stinctC) aufrufen. Wollen Sie Daten beim Auslesen mit einem Lock versehen, ist die Methode forUpdate() hilfreich, die der Abfrage den Modifikator FOR UPDATE hinzufügt. Verknüpfen von Tabellen mittels JOIN Die Klasse Zend_Db_Sel ect unterstützt auch die gesamte Bandbreite an Joins, die SQL zu bieten hat. Um die Funktionalität besser zu veranschaulichen, habe ich auf die beiden folgenden Tabellen aus einer Software zur Verwaltung von Haus- tieren zurückgegriffen. Tabelle tl ere: +-----|---------------1.----4. id | name | id_rasse | +-----4---------------1.----4. | 1 | Jake | 1 | | 2 | Ganymede | 1 | 3 | N i n c h e n | 2 | | 4 | Spongebob | NULL | +----4---------------1.-------4. Tabelle rassen: 4----4----------------------4- | id | rasse | 4----4----------------------4- | 1 | Irish Setter | 2 | Kaninchen 3 | Rhodesian Ridgeback 4 4------------------------4- Sollten Sie sich wundern, dass »Spongebob« keine Tierrasse zugeordnet ist, so liegt das daran, dass es sich dabei um einen Schwamm handelt, und Schwämme bei den Tierrassen für Haustiere leider nicht mit eingeplant waren. Die verschiedenen Joins werden nicht von allen Datenbanken unterstützt. Des Weiteren kann es bei einem Datenbanksystem auch innerhalb der einzelnen Ver- sionen Unterschiede im Verhalten geben. Prüfen Sie daher bitte, welche Mög- lichkeiten Ihre Datenbank unterstützt. Um ein JOIN zu konstruieren, sind verschiedene Methoden deklariert. Sie nutzen fast alle die gleichen Parameter, sodass die Verwendung angenehm einfach ist. Als ersten Parameter bekommen die Methoden den Namen der Tabelle überge- ben, mit welcher »gejoint« werden soll. Der zweite Parameter ist die JOI N-Bedin- 83
e-bol.net 2 | Datenbankzugriff mit Zend_Db gung. Optional können Sie danach noch angeben, welche Spalten aus der zweiten Tabelle ausgelesen werden sollen. Eine Spalte deklarieren Sie in Form eines Strings; bei mehreren Spalten greifen Sie auf ein Array zurück. Geben Sie diesen Parameter nicht an, so werden alle Spalten selektiert. Um keine Spalte aus der zweiten Tabelle zu selektieren, übergeben Sie an dieser Stelle ein leeres Array. Mit dem vierten und letzten Parameter können Sie schließlich noch ein Schema bzw. eine Datenbank angeben, falls die zweite Tabelle nicht im gleichen Kontext abgelegt sein sollte wie die erste. Wollen Sie ein einfaches INNER JO IN ausführen, können Sie auf zwei Methoden zurückgreifen. Sowohl Joint) also auch joinInner!) leisten hier dasselbe. Bei genauer Betrachtung ruft join!) auch nur joinInner!) auf. Ein sehr einfach gestalteter Aufruf könnte so aussehen: // Neues Seiect-Objekt ableiten Sselect = $db->select(): // Spalte name aus Tabelle tiere $select->from('tiere’, 'name'); // INNER JOIN mit rassen und entsprechender Bedingung $select->join('rassen', ’tiere.id_rasse = rassen.id’): $select->query(); Listing 2.14 INNER JOIN mit Zend_Db_Select Die Abfrage, die von diesen Zeilen generiert wird, lautet: SELECT 'tiere'.'name', 'rassen'.* FROM 'tiere' INNER JOIN 'rassen' ON tiere.id_rasse = rassen.id Als Ergebnis werden dabei die folgenden Daten ermittelt: name id rasse Jake 1 Irish Setter Ganymede 1 Irish Setter Ninchen 2 Kaninchen Bei der Spalte id handelt es sich um diejenige aus der Tabelle rassen. Da nicht explizit angegeben wurde, welche Spalten interessant sind, hat die Methode alle selektiert. Um nur die Spalte rasse auszulesen, würde der Aufruf von join!) so aussehen: $select->join(’rassen’, ’tiere.id_rasse = rassen.id’, 'rasse'); 84
e-bol.net Nutzung von Zend_Db I 2.2 Einen LEFT JOIN setzen Sie mit der Methode joi nl_eft() um: Sselect = $db->select(): $select->from('tiere’, ’name’): $ sei ect-> joi nl_eft(' rassen ’, 'tiere.id_rasse = rassen.id', 'rasse'): Listing 2.15 Ausführen eines LEFT JOIN Das genutzte Statement lautet folgendermaßen: SELECT 'tiere'.'name', 'rassen'.'rasse' FROM 'tiere' LEFT JOIN 'rassen' ON tiere.id_rasse = rassen.id Das ermittelte Ergebnis finden Sie in der nachfolgenden Tabelle: name rasse Jake Irish Setter Ganymede Irish Setter Ninchen Kaninchen Spongebob NULL Auf die gleiche Art und Weise können Sie auch einen RIGHT JOIN nutzen: Sselect = $db->select(): $select->from(’tiere’, ’name’): $select->joinRight(’rassen’, 'tiere.id_rasse = rassen.id', 'rasse'); Listing 2.16 Nutzung von RIGHT JOIN Hierbei wird dieser SQL-Befehl ausgeführt: SELECT 'tiere'.’name', 'rassen'.'rasse' FROM 'tiere' RIGHT JOIN 'rassen' ON tiere.id_rasse = rassen.id Er liefert dieses Ergebnis: name rasse Jake Irish Setter Ganymede Irish Setter Ninchen Kaninchen NULL Rhodesian Ridgeback Den Befehl FULL JOIN, der quasi eine Kombination aus dem LEFT und RIGHT JOIN darstellt, können Sie mit dem Befehl joinFull () ausführen. Da ich davon aus- 85
e-bol.net 2 | Datenbankzugriff mit Zend_Db gehe, dass die meisten Benutzer MySQL einsetzen, sei mir der Hinweis gestattet, dass MySQL FULL JO IN nicht unterstützt. Sselect = $db->select(): Sselect->from('tiere’, 'name'); Sselect->joinFul1('rassen', 'tiere.id_rasse = rassen.id', 'rasse'); Listing 2.17 Ausführen eines FULL JOIN Diese Zeilen führen zu dem folgenden Befehl: SELECT 'tiere'.'name', 'rassen'.'rasse' FROM 'tiere' FULL JOIN 'rassen' ON tiere.id_rasse = rassen.id Das selektierte Ergebnis sieht folgendermaßen aus: name rasse Jake Irish Setter Ganymede Irish Setter Ninchen Kaninchen Spongebob NULL NULL Rhodesian Ridgeback Die beiden JOI N-Befehle, auf die ich noch nicht eingegangen bin, sind CROSS JOIN und NATURAL JOIN. Auch für diese beiden Varianten sind eigene Methoden im schon bekannten Namensschema vorgesehen. Sie heißen joinCross() und joinNatural (). Allerdings unterscheiden sie sich ein wenig von den anderen Methoden. Aufgrund der zugrunde liegenden Ideen benötigen die beiden keine Bedingung, auf deren Basis der Befehl erstellt wird. Sie benötigen lediglich den Namen der Tabelle und akzeptieren danach noch optional die Namen der gewünschten Spalte(n) sowie den Namen der Datenbank bzw. den Namen des Schemas. CROSS JOIN, welches ein kartesisches Produkt erzeugt, wird beispielsweise so genutzt: Sselect = $db->select(); Sselect->from(’tiere’, ’name’); Sselect->joinCross('rassen', 'rasse'): Listing 2.18 CROSS JOIN mit Zend_Db_Select 86
e-bol.net Nutzung von Zend_Db | 2.2 Hieraus wird der folgende SQL-Befehl generiert: SELECT 'tiere'.'name'. 'rassen'.'rasse' FROM 'tiere' CROSS JOIN 'rassen' Das daraus resultierende kartesische Produkt sehen Sie in der nachfolgenden Tabelle: name rasse Jake Irish Setter Jake Kaninchen Jake Rhodesian Ridgeback Ganymede Irish Setter Ganymede Kaninchen Ganymede Rhodesian Ridgeback Ninchen Irish Setter Ninchen Kaninchen Ninchen Rhodesian Ridgeback Spongebob Irish Setter Spongebob Kaninchen Spongebob Rhodesian Ridgeback Das Ergebnis eines NATURAL JOIN sorgt bei der vorliegenden Tabellenstruktur zwar für Verwirrung, aber falls Sie es benötigen, so unterstützt das Zend Frame- work Sie auch an dieser Stelle: Sselect = $db->select(); $select->from(’tiere’, ’name’); $select->joinNatural(’rassen’, ’rasse’); Listing 2.19 Nutzung eines NATURAL JOIN Das dazugehörige SQL-Statement lautet: SELECT 'tiere'.'name', 'rassen'.'rasse' FROM 'tiere' NATURAL JOIN 'rassen' Da ein NATURAL JOIN zu einer Verknüpfung von zwei Tabellen über die Spalten- namen führt, lautet das Ergebnis so: name rasse Jake Irish Setter Ganymede Kaninchen Ninchen Rhodesian Ridgeback 87
e-bol.net 2 | Datenbankzugriff mit Zend_Db Sonstige Funktionalitäten Die Klasse Zend_Db_Select gibt Ihnen auch die Möglichkeit, Teile der SQL- Abfrage wieder auszulesen. Dafür ist die Methode getPartO vorgesehen. Sie bekommt eine Konstante übergeben, die definiert, welchen Teil der Abfrage Sie auslesen wollen. Hierbei sind die nachfolgenden Kostanten zulässig, die in der Klasse Zend_Db_Select deklariert sind: DISTINCT, FORJJPDATE, COLUMNS, FROM, WHERE, GROUP, HAVING, ORDER, LIMIT_COUNT, LIMIT_OFFSET. Die gleichen Konstanten können Sie auch beim Aufruf der Methode reset() nut- zen, die Teile einer bestehenden Abfrage wieder zurücksetzt. Wollen Sie also ein Objekt, das Sie bereits für eine Abfrage genutzt haben, noch einmal nutzen, kann das sehr praktisch sein. Indem Sie das Statement $select->reset(Zend_Db_ Seiect::WHERE) aufrufen, wird die WHERE-Klausel entfernt, und Sie können sie erneut setzen. 2.3 Datenbankzugriff mit Zend_Db_Table Nachdem Sie nun eine ganze Menge Möglichkeiten für einen mehr oder minder konventionellen Datenbankzugriff kennengelernt haben, möchte ich Ihnen nun Zend_Db_Tabl e vorstellen. Hierbei handelt es sich um eine Klasse, die ein soge- nanntes Active Recordset Pattern implementiert. Aber was bedeutet das konkret? Nun, die Idee ist einfach. Im Prinzip betrachtet man eine Datenbanktabelle hier- bei einfach als ein PHP-Objekt. Das heißt, die Tabelle wird mithilfe einer Klasse gekapselt. Eine solche Klasse muss eine Kind-Klasse der Klasse Zend_Db_Table oder Zend_Db_Tabl e_Abstract sein. Für welche Klasse Sie sich entscheiden, ist im Endeffekt egal, da Zend_Db_Tabl e eine Kind-Klasse von Zend_Db_Table_Ab- stract ist, aber komplett leer ist. Die Klasse muss die Information erhalten, welche Tabelle zu nutzen ist. Diese Information können Sie dadurch übergeben, dass Sie die Klasse genauso benen- nen wie die Tabelle. Ebenso ist es möglich, dass Sie den Namen der Tabelle in der Eigenschaft $_name ablegen. Bezogen auf die Tabelle kundendaten könnte das also beispielsweise so aussehen: dass Tabel 1 eKundendaten extends Zend_Db_Table { protected $_name = 'kundendaten': // Weitere Deklarationen 1 88
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 Oder so: dass kundendaten extends Zend_Db_Table { // Weitere Deklarationen } Ich würde Ihnen die erste Variante empfehlen, die ich nachfolgend auch ver- wende. Sie ist meiner Ansicht nach eindeutiger und lässt im Code besser erken- nen, dass es sich um eine Datenbankzugriffsklasse handelt. In einigen Fällen kann es vorkommen, dass der Name einer Tabelle dynamisch generiert werden muss. So gibt es hier und da den Anwendungsfall, dass jede Woche oder jeden Monat eine neue Tabelle genutzt werden soll, weil die Daten- mengen pro Tabelle sonst zu groß werden. Das könnte beispielsweise dann der Fall sein, wenn Sie Klicks auf einer Website protokollieren möchten. In so einem Fall ist es hilfreich, wenn der Name der Tabelle dynamisch von der Klasse gene- riert werden kann. Dazu können Sie die Methode _s et upTa bl eName() überladen, wobei sichergestellt sein muss, dass sie die gleichnamige Methode in der Eltern- Klasse aufruft. Die Methode _setupTableName() wird automatisch durch den Konstruktor aufgerufen. Wollen Sie beispielsweise für jeden Monat eine eigene Tabelle nutzen, so könnte die Deklaration der Methode so aussehen: dass Tabel 1 eAktuel 1 eKl ickdaten extends Zend_Db_Table { protected function _setupTableName() { $aktuel1er_monat = date('m'); $this->_name = 'kl 1ckdaten_'.$aktuel1er_monat; parent::_setupTableName(): // Weitere Deklarationen 1 Listing 2.20 Dynamisches Generieren des Tabellennamens In diesem Beispiel würde also auf eine Tabelle zugegriffen, deren Name sich immer aus kl i ckdaten_ und der Nummer des aktuellen Monats zusammensetzt, im Dezember also beispielsweise kl i ckdaten_12. Die zweite Frage bei der Initialisierung ist, wie bzw. ob Sie ein Schema, also die Datenbank, deklarieren, in der sich die Tabelle befindet. Auch hier gibt es eine Vielzahl von Möglichkeiten. Aus Gründen der Übersichtlichkeit würde ich Ihnen empfehlen, dass Sie für jede Tabelle eine eigene Klasse nutzen und die Daten- bank bzw. das Schema direkt in der Klasse fest codieren. Das ist entweder 89
e-bol.net 2 | Datenbankzugriff mit Zend_Db dadurch möglich, dass Sie die Information direkt mit an Eigenschaft $_name über- geben oder den Namen des Schemas in der Eigenschaft $_schema speichern. Um auf die Tabelle kundendaten zuzugreifen, wenn diese in der Datenbank Waren- wirtschaft liegt, können Sie also entweder die Eigenschaft $_name mit dem String 'warenwi rtschaft. kundendaten' belegen oder in $_name den String 'kundendaten' sowie in $_schema den String 'Warenwirtschaft' abspeichern. Auch hier können Sie eine Methode nutzen, um den Namen dynamisch generie- ren zu lassen. Diese Methode muss hierfür den Namen _setupMetadata() haben. Sie wird ähnlich aufgebaut wie _setupTableName(). Der Unterschied besteht darin, dass sie die Methode _setupMetadata() aus der Eltern-Klasse auf- rufen muss. Der dritte Wert, den Sie setzen sollten, ist die Eigenschaft $_pri mary. Dabei han- delt es sich um die Spalte, die den Primärschlüssel der Tabelle darstellt. Die Klasse kann zwar auch selbst versuchen zu erkennen, welches der Primärschlüs- sel ist. Aber das kostet Performance, und es könnte zudem passieren, dass die entsprechende Information nicht ermittelt werden kann. Darüber hinaus ist es für einen Dritten, der den Code lesen soll, natürlich besser zu erkennen, welches der Primärschlüssel ist, wenn dies im Code steht. Die Klasse prüft nicht, ob es sich bei der angegebenen Spalte wirklich um einen Primärschlüssel in der Daten- bank handelt. Bitte beachten Sie, dass die Klasse auf jeden Fall einen Primär- schlüssel benötigt und diesen auch nutzt. Das hat zur Folge, dass Lösch- und Update-Operationen auf Basis dieser Information ausgeführt werden. Bei einem Primärschlüssel ist festzulegen, wie dieser an den entsprechenden Wert gelangt. Zum ersten wäre es natürlich möglich, ihn manuell zu übergeben. Das kommt wahrscheinlich zumeist zum Tragen, wenn es sich um so etwas wie einen EAN-Code oder eine Personalnummer handelt, die von einem anderen Sys- tem generiert wird. Die zweite Variante, und diese wird Ihnen wahrscheinlich am ehesten vertraut sein, ist die Nutzung einer Auto-Inkrement-Spalte, wie sie von verschiedenen Datenbanksystemen zur Verfügung gestellt wird. Bei einer solchen Spalte, die auch Autowert-Spalte genannt wird, ermittelt das System selbst einen eindeutigen Wert für die Spalte. Die dritte Variante ist die Nutzung einer Sequenz. In diesem Fall muss der Wert zunächst aus einer Sequenz ausge- lesen werden und kann dann in das INSERT-Statement integriert werden. Diese Möglichkeiten deckt die Klasse ab. In allen Fällen sollten Sie die Eigen- schaft $_s eq uence mit einem korrekten Wert belegen. Fügen Sie den Wert manu- ell in das Statement ein - man spricht dabei auch von einem »Natural Key« -, so belegen Sie die Eigenschaft mit dem Wert f al se: protected $_sequence = false; 90
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 Bei der Nutzung einer Auto-Inkrement-Spalte ist die Eigenschaft mit dem boole- schen Wert true zu belegen: protected $_sequence = true: Sollten Sie beispielsweise Oracle einsetzen und eine Sequenz nutzen, dann wei- sen Sie der Eigenschaft den Namen der Sequenz zu: protected $_sequence = 'kundenndaten_sequenz': In beiden Fällen ermittelt die Klasse automatisch den Wert und fügt ihn in das Statement ein. Sollte es aus irgendwelchen Gründen notwendig sein, ist es auch hier möglich, den Namen dynamisch zu konstruieren. In diesem Fall ist die entsprechende Methode _setupPrimaryKey () zu benennen. Damit wissen Sie nun schon das wichtigste über die Deklaration einer entspre- chenden Klasse. Um auf die Tabelle zuzugreifen, benötigen Sie natürlich ein ent- sprechendes Objekt. Dabei stellt sich die Frage, wie die Klasse auf die Datenbank zugreifen kann. Auch hier gibt es wieder eine große Anzahl von Möglichkeiten. Die beiden aus meiner Sicht sinnvollsten Möglichkeiten möchte ich Ihnen jetzt vorstellen. Die erste Möglichkeit ist, dass Sie dem Konstruktor der Klasse ein ini- tialisiertes Objekt der Klasse Zend_Db übergeben: requi re_once 'Zend/Db/Table.php'; // Deklaration der Klasse dass Tabel 1 eKundendaten extends Zend_Db_Table { protected $_name = 'kundendaten': protected $_schema = 'Warenwirtschaft': protected $_primary = 'id': protected $_sequence = true: // Weitere Deklarationen 1 Soptionen = array( 'host’ => '127.0.0.1'. 'username’ => 'root', 'password’ => '', ’dbname' => '' ): // Datenbankobjekt ableiten $db = Zend_Db::factory(’mysqli',$optionen); 91
e-bol.net 2 | Datenbankzugriff mit Zend_Db // Tabellenobjekt ableiten $tbl_kundendaten = new Tabel1eKundendaten($db); Listing 2.21 Zugriff auf eine Tabelle mit Zend_Db_Table Die zweite Möglichkeit besteht darin, dass Sie den Verbindungsaufbau direkt in der Klasse mit der Methode _setupDatabaseAdapter() umsetzen. Das hat den Vorteil, dass die Kapselung besser ist. Andererseits könnte es aber verwirren, da an der Stelle, an der auf die Datenbank zugegriffen wird, nicht gleich zu erken- nen ist, in welcher Datenbank die Tabelle abgelegt ist. Eine entsprechende Im- plementation könnte beispielsweise so aussehen: requi re_once ’Zend/Db/Table.php’; dass TabelleKundendaten extends Zend_Db_Table { protected $_name = 'kundendaten'; protected $_schema = 'Warenwirtschaft'; protected $_primary = 'id'; protected $_sequence = true; protected function _setupDatabaseAdapter() { $optionen = array( 'host' => '127.0.0.1'. 'username' => ’root' , 'password’ => ’', 'dbname' => ’' ); $db = Zend_Db;;factory(’mysqli',$optionen); $ this->_setAdapter($db); parent::_setupDatabaseAdapter(); $tbl_kundendaten = new Tabel1eKundendaten(); Listing 2.22 Nutzung von _setupDatabaseAdapterO Die Ableitung des Zend_Db-Objekts erfolgt genau so, wie Sie es schon kennenge- lernt haben. Damit es korrekt eingebunden wird, wird es an die Methode _set- Adapter() übergeben. Hier gäbe es auch noch andere Wege, das Zend_Db-Objekt zu übergeben; aber auf diesem Weg werden alle sinnvollen Prüfungen mit ausge- führt. In der letzten Zeile der Methode wird die Methode _setupDatabaseAdap- 92
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 ter() der Eltern-Klasse aufgerufen, um sicherzustellen, dass die dort enthaltenen Funktionsaufrufe korrekt ausgeführt werden. In einigen Fällen kann es hilfreich sein, die Konfiguration des Zend_Db_Table- Objekts auszulesen. Die Methode info() stellt Ihnen dazu ein Array mit allen Tabelleninformationen zur Verfügung, die in dem aktuellen Objekt enthalten sind. Das heißt, Sie finden beispielsweise einen Schlüssel mit dem Namen name, der den Namen der Tabelle enthält, und einen namens col s, der die Namen aller Spalten enthält. Der Schlüssel metadata verweist auf ein indiziertes Array, in dem Sie Informationen zu den einzelnen Spalten finden. Falls Sie die Daten benö- tigen, lassen Sie das Array einfach mit var_dump() ausgeben, um die komplette Struktur zu sehen. Weitere Informationen finden Sie auch im Manual unter der folgenden URL: http://framework.zend.eom/manual/en/zend.db.table.html#zend.db.table.info Eine andere Methode, die ich noch erwähnen möchte, ist getAdapter(). Mit ihr können Sie immer eine Referenz auf das aktuelle Datenbankzugriffsobjekt, also den genutzten Adapter, auslesen. Damit haben Sie die Möglichkeit, auf die in der entsprechenden Klasse enthaltenen Methoden zurückzugreifen und beispiels- weise die quote-Funktionen zu nutzen. 2.3.1 Einfügen von Daten Um Daten in die Tabelle einzufügen, übergeben Sie den neuen Datensatz an die Methode i nsert(). Diese erwartet ein assoziatives Array, bei dem die Schlüssel des Arrays den Spaltennamen entsprechen: $tbl_kundendaten = new TabelleKundendaten(); Sdaten = array('vorname' => 'Males'. 'nachname' => "O’Brian"); $tbl_kundendaten->insert($daten); Wie Sie an diesem Beispiel schon erahnen können, kümmert die Methode sich auch darum, dass die Daten gequotet werden. Somit haben Sie an dieser Stelle kein Problem mehr. Was aber, wenn Sie eine Datenbankfunktion im Statement nutzen wollen? In dem Fall sollten Sie diese nicht einfach als Wert angeben. Sollten Sie dies den- noch tun, geht die Klasse davon aus, dass es sich um einen String handelt, quotet ihn und setzt ihn in Anführungszeichen. In den meisten Fällen dürfte dies dazu führen, dass die Funktion als String in die Spalte eingefügt und nicht vor dem Einfügen ausgeführt wird. 93
e-bol.net 2 | Datenbankzugriff mit Zend_Db Um solche Fälle nicht zu einem Problem werden zu lassen, ist die Klasse Zend_ Db_Expr vorgesehen. Die gewünschte Funktion bzw. der gewünschte Ausdruck wird als Objekt dieser Klasse übergeben. Den Ausdruck übergeben Sie dem Kon- struktor der Klasse; Sie können das Objekt auch direkt übergeben: $tbl_kundendaten = new Tabel1eKundendaten(); $expr_now = new Zend_Db_Expr('NOW()'): Sdaten = array('vorname' => 'Patrick', 'nachname' => 'Star' , 'anlegedatum' => Sexpr_now); $tbl_kundendaten->i nsert(Sdaten); Listing 2.23 Nutzung der Klasse Zend_Db_Expr Die Methode liefert übrigens immer den Wert derjenigen Spalte zurück, die als Primärschlüssel deklariert wurde. 2.3.2 Aktualisieren von Daten Möchten Sie einen Datensatz aktualisieren, so ist die Methode updateO Ihr Freund. Auch updateO bekommt, genau wie insertO, seine Daten als assozia- tives Array übergeben. Der zweite Parameter, den die Methode erwartet, ist die Bedingung, die genutzt wird, um die WHERE-Klausel zu konstruieren. Bitte beach- ten Sie, dass die Daten der gesamten Tabelle verändert werden, wenn Sie an die- ser Stelle vergessen, eine Variable zu übergeben oder einen leeren String überge- ben.2 Sie sollten also sicherstellen, dass die zweite Variable nicht versehentlich leer ist. // Neue Daten Sdaten = array('vorname' => 'Patrick', 'nachname' => 'Seestern'); // WHERE-Bedingung korrekt quoten Swhere = Stbl_kundendaten->getAdapter() ->quotelnto('id = ?', Sid); if (true — empty(Swhere)) { throw new Exception('Leere WHERE-Klausel übergeben'); } $tbl_kundendaten->update(Sdaten, Swhere); Listing 2.24 Nutzung der Methode updateO 2 In dem Fall werden zwar Warnungen ausgegeben, aber die Methode wird dennoch ausge- führt. 94
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 Auch hier gilt natürlich, dass Funktionsaufrufe in der Datenbank mithilfe eines Zend_Db_Expr-Objekts zu kapseln sind. Der Rückgabewert der Funktion ist die Anzahl der aktualisierten Zeilen. 2.3.3 Löschen von Daten Auch die Methode zum Löschen von Daten ist denkbar einfach zu handhaben. Sie bekommt nur einen Parameter übergeben, nämlich die Bedingung, die Ver- wendung in der WH ER E-Klausel findet. Auch bei dieser Methode gilt, dass Sie sicherstellen sollten, dass tatsächlich eine Bedingung übergeben wird. Übergeben Sie einen leeren String oder eine nicht initialisierte Variable, so wird der kom- plette Inhalt der Tabelle gelöscht. // Bedingung aufbauen Swhere = $tbl_kundendaten->getAdapter() ->quotelnto(’id = ?', 48); if (true — empty(Swhere)) { throw new Exception('Leere WHERE-Klausel übergeben’); } // Löschvorgang ausführen $tbl_kundendaten->delete($where); Listing 2.25 Löschen von Datensätzen Möchten Sie wissen, wie viele Zeilen gelöscht wurden, ist dies kein Problem. Denn die Methode gibt die Anzahl der entfernten Zeilen zurück. 2.3.4 Auslesen von Daten Nachdem Sie nun wissen, wie Sie Daten in einer Tabelle speichern bzw. diese manipulieren, stellt sich noch die Frage, wie Sie die Daten wieder auslesen kön- nen. Es gibt verschiedene Methoden, mit denen Sie Daten aus der Tabelle ausle- sen können. Allerdings haben alle etwas gemeinsam: Sie liefern kein Array zurück, sondern ein Objekt der Klasse Zend_Db_Table_Rowset. Das Rowset- Objekt enthält wiederum einzelne Objekte der Klasse Zend_Db_Table_Row, wel- che die Daten enthalten. Eine Ausnahme stellt die Methode fetchRow() dar, die immer exakt eine Zeile zurückgibt und somit sofort ein Row-Objekt nutzt. Dazu erfahren Sie später mehr. Die vielleicht einfachste Methode, um Daten zu selektieren, ist die Methode findO. Sie »sucht« innerhalb der Tabelle auf Basis des Primärschlüssels. Das heißt, sie bekommt einen Wert übergeben, den sie anschließend mit dem Pri- märschlüssel abgleicht. Sollte ein Datensatz gefunden werden, gibt sie ihn als 95
e-bol.net 2 | Datenbankzugriff mit Zend_Db Zend_Db_Tabl e_Rowset-Objekt zurück. Ein Rowset deswegen, weil Sie der Methode auch mehrere Werte in Form eines Arrays übergeben können. Ein Auf- ruf könnte so aussehen: $tbl_kundendaten = new Tabel1eKundendaten(); Srowset = $tbl_kundendaten->find( 52); $row = $rowset->current(); echo "ID: ".$row->id: echo "<br>Vorname: ".$row->vorname; echo "<br>Nachname: ".$row->nachname: Listing 2.26 Selektieren von Daten In diesem Fall wird exakt ein Datensatz gefunden, da der Methode nur ein Wert übergeben wurde. Da es exakt ein Row-Objekt war, das in dem Rowset-Objekt enthalten ist, können Sie dieses mithilfe der Methode current!) auslesen. Das Rowset-Objekt implementiert übrigens das SPL-Interface Iterator, sodass es auch direkt in einer foreach-Schleife genutzt werden kann. Nachdem die Referenz auf das Row-Objekt ausgelesen wurde, können Sie auf die darin enthaltenen Werte zugreifen, indem Sie die Namen der Spalten als Eigen- schaften nutzen. Die Ausgabe des Scripts sehen Sie in Abbildung 2.3. ~ http:/.,,b/3.php CD ID: 52 Vorname: Milcs Nachname: O'Brian Abbildung 2.3 Ausgabe von selektierten Daten Sollten Sie die Nutzung von Arrays bevorzugen, ist dies auch kein Problem. Sowohl das Rowset-Objekt als auch das Row-Objekt kennen die Methode toAr- ray(). Rufen Sie diese bei dem Rowset-Objekt auf, so erhalten Sie ein zwei- dimensionales Array. In der ersten Dimension ist dieses indiziert, wobei jedes Element eine komplette Tabellenzeile darstellt. Die zweite Ebene nutzt die Namen der Tabellenspalten als Schlüssel und enthält die Werte der jeweiligen Zeile. Rufen Sie die Methode aus einem Row-Objekt heraus auf, erhalten Sie ein indiziertes Array, bei dem die Spaltennamen auf den jeweiligen Wert verweisen. 96
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 Eine zweite Möglichkeit, um Werte aus der Tabelle auszulesen, stellt die Methode fetchAl 1 () dar. Übergeben Sie ihr keinen Parameter, liefert sie Ihnen den kompletten Inhalt der Tabelle zurück. Allerdings können Sie ihr auch einige Parameter übergeben. Wenn Sie ihr an erster Stelle eine Bedingung in Form eines Strings übergeben, wird sie in einer WHERE-Klausel genutzt. Mit dem zwei- ten Parameter können Sie eine Sortierreihenfolge bestimmen. Hier existieren mehrere Möglichkeiten. Übergeben Sie nur einen Spaltennamen, so wird ent- sprechend dieser Spalte aufsteigend sortiert. In der zweiten Variante übergeben Sie einen Spaltennamen gefolgt von der Sortierrichtung, also ASC oder DESC. Zugegebenermaßen stellt das keinen großen Unterschied zu der ersten Variante dar, da in der ersten Variante die Angabe ASC implizit von der Klasse ergänzt wird. Die dritte Möglichkeit ist, dass Sie mehrere Spaltennamen mit oder ohne Sortierreihenfolge in einem Array übergeben. Mit den Parametern drei und vier können Sie ein LIMIT realisieren. Der erste der beiden gibt an, wie viele Werte ausgelesen werden sollen. Der zweite definiert den Offset, liefert also die Infor- mation, wie viele Zeilen übersprungen werden sollen. Swhere = 'anlegedatum > "2007-10-12"'; Ssortierung = array ('nachname ASC, 'vorname ASC'); Sanzahl = 10; Soffset = 20; Srowset = Stbl_kundendaten->fetchAl1(Swhere, Ssortierung, Sanzahl, Soffset); Mit diesen Zeilen wird ein SELECT-Statement generiert, das zehn Datensätze aus- liest und die ersten 20 überspringt. Dabei werden nur Daten selektiert, bei denen das Anlegedatum »größer« als der 12.10.2007 ist. Sortiert wird in erster Ebene nach dem Nachnamen und in zweiter Instanz nach dem Vornamen. Die Methode fetchRow() liefert immer exakt eine Zeile zurück. Somit hat es auch keinen Sinn, mit einem Rowset zu arbeiten. Die Methode liefert nur ein Objekt der Klasse Zend_Db_Table_Row. Die genutzte SQL-Abfrage kann allerdings auch mehrere Treffer liefern. In dem Fall wird nur die erste Zeile aus der Ergebnis- menge zurückgegeben. Um die Abfrage zu präzisieren, können Sie der Methode als ersten Parameter eine Bedingung für ein WH ERE mit auf den Weg geben. Der zweite Parameter, der ebenfalls optional ist, bestimmt, wie die Ergebnisse zu sor- tieren sind. Hierbei existieren die gleichen Möglichkeiten, wie sie schon bei fetchAl l () beschrieben wurden. Sollte die Abfrage kein Ergebnis ermitteln, gibt die Methode - im Gegensatz zu den beiden anderen Methoden, die immer ein Rowset-Objekt liefern - den Wert nul l zurück. 97
e-bol.net 2 | Datenbankzugriff mit Zend_Db Erweiterte Techniken mit Zend_Db_Table_Rowset und Zend_Db_Table_Row Die Zend_Db_Rowset- und Zend_Db_Table_Row-Objekte können noch einiges mehr als das bisher Besprochene. Was sicherlich besonders spannend ist, ist die Tatsache, dass Sie mithilfe von Row-Objekten Daten auch manipulieren und wie- der abspeichern können. Zuerst aber zu dem, was die Rowset-Klasse noch so zu bieten hat. Nachdem Sie eine Abfrage ausgeführt haben, stellt sich die Frage, ob Sie ein Ergebnis erhalten haben bzw. wie viele Zeilen darin enthalten sind. Da die Methoden fi nd () und fetchAl 1 () immer ein Rowset-Objekt liefern, das auch leer sein kann, ist diese Frage nicht unerheblich. Die Methode exi sts() bestätigt Ihnen durch Rückgabe des booleschen Wertes true, dass Zeilen gefunden wurden und im Objekt ent- halten sind. Sollten Sie den Wert false erhalten, ist das Objekt leer. Die exakte Anzahl der Zeilen, die enthalten sind, ermittelt die Methode count(). Darüber hinaus sind noch die Methoden definiert, die durch das SPL-Interface Iterator vorgeschrieben sind, und einige weitere Methoden, mit denen Sie das Table- Objekt auslesen können. Wie schon angedeutet, können Sie mithilfe des Row-Objekts auch Daten manipu- lieren. Im einfachsten Fall verändern Sie die Werte, die in den Eigenschaften gespeichert sind, einfach direkt und rufen die Methode save() auf. Sie speichert den Datensatz bzw. führt den UPDATE-Befehl aus. Interessant in diesem Zusam- menhang ist auch die Methode setFromArray (). Sie bekommt ein Array überge- ben, bei dem die Schlüssel den Spaltennamen der Tabelle entsprechen. Die Werte werden dabei automatisch übernommen, sodass Sie alle Werte auf einmal setzen können. Um eine Zeile zu löschen, rufen Sie die Methode del ete() aus dem Row-Objekt heraus auf, das gelöscht werden soll. Genau genommen wird natürlich nicht nur das Row-Objekt, sondern auch die entsprechende Zeile in der Datenbank gelöscht. An dieser Stelle möchte ich noch einmal nachdrücklich darauf hinwei- sen, dass Sie einen gültigen Primärschlüssel benötigen, da die Löschoperation auf Basis dieses Schlüssels ausgeführt wird. Das heißt, wenn Sie eine Spalte als Pri- märschlüssel deklariert haben, in der Werte doppelt vorkommen, so werden alle Zeilen gelöscht, die den gleichen Wert in der Spalte des »Primärschlüssels« ver- wenden. Bitte beachten Sie, dass Sie nach jeder Veränderung einer Zeile die Daten im Rowset-Objekt wieder aktualisieren müssen, da die Operationen direkt in der Datenbank stattfinden. Das folgende Beispiel zeigt das Zusammenspiel der Methoden: 98
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 // Tabellenobjekt ableiten $tbl_kundendaten = new Tabel1eKundendaten(); // Alle Zeilen auslesen Srowset = $tbl_kundendaten->fetchAl1(); // Anzahl der Zeilen ermitteln $anzahl_zei1 en = $rowset->count(): echo "<p>Anzahl Zeilen: ".$anzahl_zei1 en; echo "<table>"; foreach ($rowset as $row) { // Zeile ausgeben echo "<tr> <td>$row->id</td> <td>$row->vorname</td> <td>$row->nachname</td> </tr>": // Zeile löschen $row->delete(); } echo "</tableX/p>": // Neue Zeile anlegen $expr = new Zend_Db_Expr('NOW()'): Sdaten = array ('vorname' => 'Anika', 'nachname' => 'Perroquet', ’anlegedatum’ => $expr); // Daten einfügen $id = $tbl_kundendaten->insert($daten); // Rowset neu auslesen Srowset = $tbl_kundendaten->fetchAl1(); $anzahl_zei1 en = $rowset->count(): echo "<p>Anzahl Zeilen: ".$anzahl_zei1 en; echo "<table>"; foreach ($rowset as $row) { echo "<tr> <td>$row->id</td> <td>$row->vorname</td> <td>$row->nachname</td> </tr>": } echo "</table></p>": Sadapter = $tbl_kundendaten->getAdapter(); Swhere = $adapter->quotelnto('id = ?',$id): 99
e-bol.net 2 | Datenbankzugriff mit Zend_Db // Zuletzt eingefügte Zeile via id auslesen $row = $tbl_kundendaten->fetchRow(Swhere); // Spalte nachname ändern $row->nachname ='Papagei’; // Daten speichern $row->save(); // Datensatz neu auslesen Srowset = $tbl_kundendaten->fetchAl1(); echo "<p><table>"; foreach (Srowset as $row) { echo "<tr> <td>$row->id</td> <td>$row->vorname</td> <td>$row->nachname</td> </tr>"; } echo "</table></p>"; Listing 2.27 Manipulation von Datensätzen mit Zend_Db_Table_Row Die Ausgabe von Listing 2.27 sehen Sie in Abbildung 2.4. n n n http://127.0....-buch/Db/4.php O Anzahl Zeilen: 3 49 Miles O'Brian 70 Spongcbob Squarepants 69 Patrick Star Anzahl Zeilen: 1 76 Anika Pcrroquct 76 Anika Papagei Abbildung 2.4 Mit Zend_Db_Table_Row manipulierte Daten Abhängigkeiten Nachdem Sie nun schon das meiste über die Datenbankklassen wissen, möchte ich Ihnen noch eine wirklich praktische Funktionalität vorstellen, die Code in vielen Fällen besser lesbar macht. Bei den meisten datenbankgestützten Anwendungen hängen Tabellen voneinan- der ab. Resultierend aus der Normalisierung der Datenstrukturen sind Informati- 100
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 onen über mehrere Tabellen verteilt. Ein Beispiel dafür sind beispielsweise die Kundeninformationen in einem Shop-System. Jeder Kunde sollte nur einmal angelegt sein. Da er allerdings mehrfach Ware bestellen könnte, sollten die Bestellinformationen in einer anderen Tabelle gepflegt werden. Jede Bestellung kann dann noch weitere Produkte enthalten. Das es auch wenig sinnvoll wäre, die Produktinformationen direkt an die Bestellungen zu hängen, werden die Arti- kelstammdaten in einer gesonderten Tabelle verwaltet. Solche Tabellen manuell abzufragen, kann recht umständlich werden. Mit Zend_ Db haben Sie allerdings ein recht mächtiges Mittel zur Hand, das Ihnen viel Arbeit abnimmt. Für die Beispiele habe ich vier Tabellen benutzt. Das ist zum ersten die Tabelle künden, welche die Stammdaten der Kunden verwaltet: CREATE TABLE 'künden' ( 'id' int(ll) NOT NULL AUTO_INCREMENT, 'vorname' varchar(lOO) DEFAULT NULL, 'nachname' varchar(lOO) DEFAULT NULL, PRIMARY KEY ('id' ) ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Die Bestellungen der Kunden finden sich in der Tabelle bestel 1 ungen. Welcher Kunde welche Bestellungen ausgelöst hat, wird über die Spalte id_kunde festge- halten, in welcher der Primärschlüssel aus der Tabelle künden genutzt wird. Auf- gebaut ist die Tabelle so: CREATE TABLE 'bestel1ungen' ( 'id' int(ll) NOT NULL AUT0_INCREMENT, 'id_kunde' int(ll) NOT NULL, 'bestel1 datum' datetime DEFAULT NULL, PRIMARY KEY ('id' ) ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Auch die Tabelle zur Verwaltung der Artikelstammdaten, die den Namen Pro- dukte hat, ist recht einfach aufgebaut: CREATE TABLE 'produkte' ( 'id' int(ll) NOT NULL AUT0_INCREMENT, 'bezeichnung' varchar(lOO) DEFAULT NULL, PRIMARY KEY ('id' ) ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Nun fehlt noch eine Tabelle, die die Produkte den Bestellungen zuordnet. Dabei handelt es sich um eine kleine Verknüpfungstabelle, die bestel 1 ungenProdukte heißt und nur die jeweiligen IDs speichert, um eine Verknüpfung herzustellen: 101
e-bol.net 2 | Datenbankzugriff mit Zend_Db CREATE TABLE 'bestel1ungenProdukte' ( 'id_bestel1ung' Int(ll) NOT NULL, ~id_produkt' int(ll) NOT NULL ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Da diese Tabelle keine ID hat, werde ich die beiden Spalten als einen zusammen- gesetzten Schlüssel nutzen. Das würde für einen realen Einsatz natürlich wenig Sinn ergeben, da bei dieser Konstruktion ein Produkt in jeder Bestellung nur ein- mal vorkommen darf. Sie sollten diese Tabellen also nicht unbedingt als Grund- lage für einen Shop nutzen. Nachdem Sie nun die Tabellen kennen, ist zu überlegen, wie diese Tabellen mit- hilfe von Zend_Db miteinander verknüpft werden. Zunächst einmal müssen Sie die Klassen für die Tabellen ganz normal auf Basis von Zend_Db_Table bzw. Zen d_Db_Ta b 1 e_Ab s t ra c t deklarieren. Allerdings müssen Sie noch einige Dinge zusätzlich deklarieren. Da wäre zunächst die Eigenschaft $_dependentTabl es. Mit ihr geben Sie an, welche Tabel- len von der Tabelle abhängen, deren Klasse Sie gerade deklarieren. Bezogen auf das oben geschilderte Szenario würde also in der Klassendeklaration für die Tabelle künden die Tabelle bestel 1 ungen in dieser Eigenschaft angegeben, da sie von der ID des Kunden abhängig ist. Diese Eigenschaft können Sie mit einem String oder einem Array belegen, falls es sich um mehrere voneinander abhän- gige Tabellen handeln sollte. Bei der Deklaration einer abhängigen Tabelle müssen Sie noch festlegen, wie sie von einer anderen Tabelle abhängt. Dazu werden Regeln definiert, die in Form eines assoziativen Arrays in der Eigenschaft $_referenceMap abgelegt werden. Jede der Regeln erhält einen eigenen Namen und besteht wiederum aus einem Array. Dieses Array muss mindestens die Schlüssel 1 col umns1, ’ refTabl eCl ass ’ und ' ref Col umn ’ enthalten. Der erste definiert dabei, welche Spalten in der aktu- ellen Tabelle, deren Klasse Sie gerade definieren, von der anderen Tabelle abhän- gen. Mit ’ refTabl eCl ass' geben Sie den Namen der Klasse an, welche für die referenzierte Tabelle zuständig ist. Bitte beachten Sie, dass es sich dabei um den Namen der Klasse und nicht um den Namen der Tabelle handelt. Der letzte Schlüssel ’ refCol umn ’ definiert den Namen der Spalte, die den Wert enthält, der referenziert wird. Bei der ersten sowie der letzten Eigenschaft können Sie übri- gens auch ein Array nutzen, um die Namen der Spalten anzugeben, was aber wohl nicht so oft vorkommen dürfte. Damit besitzen alle Klassen die notwendigen Informationen, um eine Beziehung zwischen den Daten herzustellen. Optional können Sie allerdings noch weitere Informationen in den Arrays angeben, welche die Regeln definieren. Aber Vor- 102
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 sicht: Diese sollten Sie nur dann verwenden, wenn die von Ihnen genutzte Datenbankarchitektur keine »Deklarative Referentielle Integrität« (DRI) unter- stützt. DRI bedeutet, dass Sie beim Anlegen der Tabellen gleichzeitig definieren können, dass eine Tabelle von einer anderen abhängt. Dabei können Sie auch festlegen, wie sich eine abhängige Tabelle verhalten soll, wenn eine Zeile gelöscht wird. Konkret hieße das hier beispielsweise: Was soll in der Tabelle bestell ungen passieren, falls ein Kunde, zu dem es Bestellungen gibt, gelöscht wird? Würden die Bestellungen in der Tabelle erhalten bleiben, wären die Daten inkonsistent, da der dazugehörige Kunden, der durch die ID in der Spalte i d_ künde referenziert wird, nicht mehr existiert. Datenbanksysteme mit DRI unter- stützen hier ein kaskadierendes Löschen. Damit sorgt das Datenbanksystem auto- matisch dafür, dass die abhängigen Daten gelöscht werden. DRI wird von MySQL zurzeit nur dann unterstützt, wenn Sie InnoDB als Datenbank-Engine nutzen. Es wäre also die bessere Variante, wenn Sie auf InnoDB setzen und der Datenbank die Pflege der referentiellen Integrität überlassen würden. Ich habe hier bewusst auf MylSAM gesetzt, damit ich Ihnen die Funktionalitäten von Zend_Db vorstel- len kann.3 Sollte Ihr Datenbanksystem kein DRI unterstützen, oder sollten Sie MylSAM aus irgendwelchen Gründen den Vorzug geben, so können Sie die beiden Schlüssel ’onDelete' und ’onUpdate' bei der Deklaration der Regeln nutzen. Der erste definiert, was passieren soll, wenn in der Tabelle, die referenziert wird, ein Datensatz gelöscht wird. Der zweite bestimmt, was bei einem UPDATE in der »Eltern«-Tabelle passiert. Diesen beiden Schlüsseln können Sie entweder sei f::CASCADE oder sei f:: RESTRICT zuweisen. Mit dem ersten Wert wird ein kaskadierendes Verhalten bewirkt. Geben Sie 'onDelete' => seif:: CASCADE an, so führt ein Löschen in der Eltern-Tabelle dazu, dass der dazugehörige Datensatz bzw. die dazugehörigen Datensätze in der Kind-Tabelle gelöscht werden. Glei- ches gilt für den Schlüssel ’onUpdate’ bei einem UPDATE, das in der Eltern- Tabelle ausgeführt wird. Mit der Konstanten seif::RESTRICT können Sie dies verhindern. Geben Sie diesen optionalen Schlüssel nicht an, so führt Zend_Db übrigens auch keine weitergehenden Aktionen aus. Nach so viel Vorrede hier nun der Code, der die Klassen deklariert: requi re_once ’Zend/Db/Table.php’; // Klasse um die Verbindungsdaten nur ein Mal // deklarieren zu müssen 3 Leider sind diese Funktionalitäten momentan (ZF Version 1.0.3) noch nicht zuverlässig im- plementiert. Bitte testen Sie vorher, ob kaskadierende Lösch- oder Update-Vorgänge funktionie- ren. 103
e-bol.net 2 | Datenbankzugriff mit Zend_Db dass Verbindungsdaten extends Zend_Db_Table { protected $_name = 'kundendaten'; protected $_schema = 'Warenwirtschaft'; protected $_primary = 'id'; protected $_sequence = true; protected function _setupDatabaseAdapter() { $optionen = array( 'host' => '127.0.0.1'. 'username' => ’root'. 'password' => ’'. 'dbname' => 'Warenwirtschaft' ); $db = Zend_Db;:factory(’mysqli',$optionen); $ this->_setAdapter($db); parent::_setupDatabaseAdapter(); } // Zugriffsklasse für Tabelle künden dass tabel 1 eKunden extends Verbindungsdaten { protected $_name = 'künden'; protected $_primary = 'id'; protected $_dependentTables = 'bestel1ungen’; protected $_sequence = true; } // Zugriffsklasse für Tabelle bestellungen // diese Tabelle hängt von Tabelle künden ab // und die Tabelle bestel1ungenProdukte haengt // von bestellungen ab dass tabel 1 eBestel 1 ungen extends Verbindungsdaten { protected $_name = 'bestellungen'; protected $_primary = 'id'; protected $_sequence = true; // Klasse der Tabelle, die von dieser abhängt protected $_dependentTables = 'bestel1ungenProdukte’; protected $_referenceMap = array( // Regel für die Abhaengigkeit von künden 'Kunde' => array( 104
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 ’columns’ => ’id_kunde', ’refTableClass' => 'tabel1eKunden', ’refColumn' => 'id', ’onDelete' => seif::CASCADE, ’onUpdate' => seif::CASCADE // Zugriffsklasse für Tabelle produkte dass tabel 1 eProdukte extends Verbindungsdaten { protected $_name = 'produkte': protected $_primary = 'id': protected $_sequence = true: // Tabelle bestel1ungenProdukte hängt von dieser ab protected $_dependentTables = 'bestel1ungenProdukte’: } // Zugriffsklasse für Tabelle bestel1ungenProdukte dass tabel 1 eBestel 1 ungenProdukte extends Verbindungsdaten { protected $_name = 'bestel1ungenProdukte': // Zusammengesetzter Schlüssel protected $_primary = array('id_bestel1ung','id_produkt'): protected $_referenceMap = array( 'Bersteilungen' => array( ’columns’ => 'id_bestel1ung', 'refTableClass' => 'tabel1eBestel1ungen’, 'refColumn' => 'id', ’onDelete' => seif::CASCADE, ’onUpdate' => seif::CASCADE ), 'Produkte' => array( ’columns’ => 'id_produkt', 'refTableClass' => 'tabel1eProdukte’, 'refColumn' => 'i d', ’onDelete' => seif::CASCADE, ’onUpdate' => seif::CASCADE ) ); } Listing 2.28 Deklaration von abhängigen Tabellen 105
e-bol.net 2 | Datenbankzugriff mit Zend_Db Damit ist die Deklaration abgeschlossen. Nun stellt sich noch die Frage, welchen Vorteil das alles bringt. - Stellen Sie sich vor, dass Sie die Bestellungen des Kunden Homer Simpson auslesen wollen. Dieser Kunde hat in diesem Beispiel die ID 1. Zunächst wird die Zeile ausgelesen, die zur ID 1 aus der Tabelle künden gehört. Sobald Sie das entsprechende Zend_Db_Tabl e_Row-Objekt ausgelesen haben, können Sie die Methode f1 ndDependentRowsetf) die Zeilen auslesen lassen, die davon abhängig sind. Dazu übergeben Sie der Methode den Namen der Tabellen- klasse, die für die Kind-Tabelle definiert ist: $tbl_kunden = new tabel1eKunden(): // Kunden auslesen Skunde = $tbl_kunden->find(1): // Row-Objekt auslesen Skunde = $kunde->current(): // Abhängige Bestellungen auslesen Jbestel1ungen = $kunde - >f 1ndDependentRowsett’tabelleBestellungen'); echo "Bestellungen von: $kunde->vorname $kunde->nachname<br>"; foreach (Sbestel1ungen as tbestellung) { echo "<p>id: $bestel1ung->id"; echo "CbOBestel Idatum: $bestel 1 ung->bestel ldatum</p>" ; 1 Listing 2.29 Auslesen von abhängigen Zeilen Die Ausgabe, die dieses Listing generiert, sehen Sie in Abbildung 2.5. A O O http://127.0.0.1/~..en/zf-buch/Db/6.php ' » Bestellungen von: Homer Simpson id: 1 Bcstclldatum: 2007-11-12 12:34:29 J Abbildung 2.5 Ausgabe einer abhängigen Zeile Wie Sie sehen, hat die Klasse selbstständig die abhängigen Daten gefunden. Dass in diesem Fall nur ein Datensatz ermittelt wurde, liegt daran, dass Herr Simpson noch nicht mehr bestellt hat. 106
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 Ein anderer Fall, der hier nicht zum Tragen kommt, könnte sein, dass es mehrere Abhängigkeiten zwischen zwei Tabellen gibt. In dem Fall müssten Sie auch meh- rere Regeln definieren. Damit der Aufruf von fi ndDependentRowset() eindeutig bleibt, müssen Sie als zweiten Parameter noch den Namen der Regel, die genutzt werden soll, angeben. Das Zend Framework sieht an dieser Stelle noch eine andere Syntax vor. Und zwar können Sie den Namen der Methode selbst »konstruieren«. Setzen Sie den Namen einfach aus dem Schlüsselwort find und dem Namen der Klasse, auf deren Tabelle sie zugreifen wollen, zusammen. In diesem Fall hätte der Funkti- onsaufruf $kunde->findtabel 1 eßestel 1 ungen(); lauten müssen. Auch hier können Sie den Namen einer Regel angeben, sofern das nötig sein sollte. Den Namen der Regel hängen Sie dann, abgetrennt durch das Schlüsselwort By, an den Namen der Klasse an. In diesem Beispiel würde sich der folgende Aufruf ergeben: Sbestel1ungen = $kunde->findtabel1eßestel1ungenByKunde(): Ob diese Vorgehensweise die Lesbarkeit des Codes verbessert, muss jeder für sich selbst entscheiden. Das Ganze funktioniert übrigens aus in umgekehrter Richtung. Wenn Sie Daten aus der Tabelle bestellungen ausgelesen haben und herausfinden wollen, wel- cher Kunde dazu gehört, ist das kein Problem. In diesem Fall ist die Methode findParentRow() Ihr Kandidat. Sie liefert zu einem Row-Objekt die dazugehö- rige Zeile aus der Eltern-Tabelle. Das nächste Listing liest alle Bestellungen aus und liefert zu jeder Bestellung die Angabe, wer sie getätigt hat: $tbl_bestel1ungen = new tabelleßestel1ungen(); Sbestel1ungen = $tbl_bestel1ungen->fetchAl1(); foreach ($bestel1ungen as $bestellung) { tkunde = $bestel1ung->findParentRow('tabel1eKunden'); echo "<p>Bestel1ung: $bestel1ung->id": echo "<br>Bestel1 datum: $bestel1ung->bestelIdatum"; echo "<br>Kunde: $kunde->vorname $kunde->nachname</p>": } Listing 2.30 Auslesen einer »Eltern-Zeile« Die Methode benötigt den Namen der Klasse, die für die Eltern-Tabelle zuständig ist, als Parameter. Zusätzlich können Sie auch den Namen einer Regel angeben. 107
e-bol.net 2 | Datenbankzugriff mit Zend_Db Auch hier können Sie die schon oben beschriebene alternative Syntax nutzen. Hierbei leiten Sie den Namen der Methode allerdings nicht nur mit find, son- dern mit fi ndParent ein. Somit würden die Methodennamen in diesem Beispiel fi ndParenttabel 1 eKunden() und findParenttabelleKundenByKunde!) lauten. Die letzte und sicher interessanteste Methode in diesem Zusammenhang ist fi ndManyToManyRowset!). Sie ist in der Lage, Daten auszulesen, die über eine m.n-Verknüpfung verbunden sind. Eine solche m.n-Verknüpfung liegt in diesem Beispiel bei der Verknüpfung der Bestellungen und der Produkte vor. Die Methode bekommt den Namen der Zieltabelle (produkte) und den Namen der Verknüpfungstabelle (bestel 1 ungenProdukte) übergeben. Genau genommen natürlich nicht den Namen der Tabellen, sondern die Namen der Zend_Db_Tabl e- Klassen, die für den Zugriff auf die Tabellen verwendet werden. Optional können Sie als dritten und vierten Parameter noch Namen von Regeln übergeben, die genutzt werden sollen. Die Methode liefert dann ein Rowset mit den entspre- chenden Zeilen aus der Zieltabelle zurück. Im folgenden Beispiel werden zunächst alle Kunden ausgelesen, dann alle Bestel- lungen der Kunden und darauf basierend die Produkte, die zu der jeweiligen Bestellung gehören: $tbl_kunden = new tabel1eKunden(); Skunden = $tbl_kunden->fetchAl1(); foreach (tkunden as $kunde) { // Bestellungen pro Kunde auslesen tbestel1ungen = $kunde->findDependentRowset ('tabel1eBestel1ungen'); echo "Bestellungen von: $kunde->vorname $kunde->nachname<br>"; foreach ($bestel1ungen as $bestellung) { echo "<p>id: tbestel1ung->id"; echo "<br>BestelIdatum: $bestel1ung->bestelIdatum"; echo "<br>Artikel:<ul>": // Produkte pro Bestellugn auslesen // Zieltabelle = 'tabel1eProdukte' // Verknüpfungstabelle = 'tabel1eBestel1ungenProdukte' $produkte = $bestel1ung->findManyToManyRowset( 'tabelleProdukte', 'tabel1eBestel1ungenProdukte'); foreach (tprodukte as $produkt) echo "<1i>$produkt->bezeichnung</li>": 108
e-bol.net Datenbankzugriff mit Zend_Db_Table | 2.3 echo "</ul><hr></p>"; I } Listing 2.31 Auslesen einer m:n-Abhängigkeit Die generierte Darstellung sehen Sie in Abbildung 2.6. ° http://127.0.0.l/~c en/zf-buch/Db/6.php CD U W ö w Bestellungen von: Homer Simpson id: 1 Bestelldatum: 2007-11-12 12:34:29 Artikel: • Ausreden für Sicherheitsingenieure • Bleiwestc gegen atomare Strahlung Bestellungen von: Apu Nahasapccmapctilon id: 2 Bestendatum: 2007-11-11 19:41:59 Artikel: • 24h wach bleiben - Leitfaden für Ladenbesitzer • Verändern von Haltbarkcitsdatcn • Verklagt! Was nun? Abbildung 2.6 Daten aus einer m:n-Verknüpfung Auch an dieser Stelle können Sie wieder die schon beschriebene alternative Syn- tax nutzen. Hierbei benutzen Sie das Schlüsselwort find und geben dann den Namen der Zieltabelle, gefolgt von Vi a und dem Namen der Verknüpfungstabelle an. Auch hier sind natürlich die Namen der Klassen zu gebrauchen. In diesem Beispiel würde der Methodenaufruf wie folgt aussehen: $bestellung->findtabelleProdukteVi ata bei 1eBestel1ungenProdukte(): Möchten Sie noch explizit eine Regel angeben, so können Sie By anhängen und danach den Namen der Regel ergänzen. Benötigen Sie danach noch eine zweite Regel, so geben Sie And und dann den Namen der zweiten Regel an. 2.3.5 Kaskadierende Lösch- und Update-Vorgänge Wie eingangs bereits erwähnt, unterstützt Zend_DB_Tabl e auch kaskadierende Lösch- und Update-Vorgänge, falls Ihr Datenbankserver kein DRI unterstützen 109
e-bol.net 2 | Datenbankzugriff mit Zend_Db sollte. Haben Sie die Schlüssel onUpdate und onDelete bei den Regeln mit der Konstanten sei f:: CASCADE belegt, kümmert sich die Klasse automatisch um alles Notwendige. Hierbei ist allerdings zu beachten, dass die Änderungen nur dann ausgeführt werden, wenn die Änderungen über die Row-Objekte vorgenommen werden. Das heißt, wenn Sie Änderungen mit save() speichern oder eine Zeile mit del ete() löschen, werden auch die abhängigen Daten geändert. Dies bedeu- tet: Würden Sie so die ID eines Kunden ändern, würde auch die Spalte id_kunde in der Tabelle bestel 1 ungen aktualisiert: Skunden = $tbl_kunden->find(2); Skunde = $kunden->current(): $kunde->id = 668; $kunde->save(); 2.4 Performanceanalyse mit Zend_Db_Profiler Bei umfangreichen Anwendungen ist die Geschwindigkeit der Datenbankanfra- gen oft ein entscheidendes Problem. Stellen Sie fest, dass eine Anwendung zu langsam ist, so gibt es verschiedene Strategien, wie Sie vorgehen können. Um Sie an dieser Stelle zu unterstützen, ist in Zend_Db ein Profiler integriert. Dieser kann für Sie protokollieren, wie lange die Ausführung der Abfragen gedauert hat. Sie können den Profiler auf verschiedenste Weisen einschalten. Ich möchte Ihnen hier nur zwei Möglichkeiten vorstellen. Weitere finden Sie in der Doku- mentation zu Zend_Db_Profi1 er. Die erste Möglichkeit ist, dass Sie dem Zend_Db-Objekt, das Sie ableiten, die ent- sprechende Information direkt übergeben. Und zwar können Sie dazu in den Optionen für den Verbindungsaufbau zusätzlich den Schlüssel profi 1 er mit dem Wert true belegen. Die zweite Variante - und das ist auch diejenige, die ich hier nutzen werde - besteht darin, den Profiler über einen Methodenaufruf zu aktivieren. Dazu müs- sen Sie zunächst eine Referenz auf den Profiler auslesen, was Sie mit getProf i - 1 er() machen können. Mit dieser Objektreferenz können Sie die Methode set- Enabled() aufrufen, der Sie ein true oder false übergeben, um den Profiler ein- bzw. auszuschalten. Bitte setzen Sie den Profiler nur gezielt ein, da er das System langsamer macht. Er sollte daher nicht ständig im Hintergrund mitlaufen. Eine Nutzung könnte beispielsweise so aussehen: $db = Zend_Db::factory(’mysqli',$optionen); // Referenz auf Profiler auslesen Sprofiler = $db->getProfiler(); 110
e-bol.net Performanceanalyse mit Zend_Db_Profiler | 2.4 // Profiler einschalten $profiler->setEnabled(true); // 1000 INSERTs ausführen for ($i = 0; $i<1000; $i++) { $db->query("INSERT INTO plz (plz) VALUES (?)", mt_rand(1000.99999)); I // 1000 SELECTs ausführen for ($i = 0; $i<1000; $i++) { $db->query("SELECT * FROM plz WHERE id = ? OR id = ?". array(mt_rand(l,1000), mt_rand(1,1000))); } // Ergebnisse des Profilers ausgeben echo "Gesamtzahl der Abfragen: ".$profi1er->getTotalNumQueriesI); echo "<br>- Anzahel der INSERTs: "; echo $profiler->getTotalNumQueries(Zend_Db_Profi1er::INSERT); echo "<br>- Anzahel der SELECTs: "; echo $profiler->getTotalNumQueries(Zend_Db_Profi1er::SELECT); echo "<br>Dauer aller Abfragen: echo $profi1er->getTotalElapsedSecsf)." Sekunden"; echo "<br>- Dauer der INSERTs: "; echo Sprofi1er->getTotalElapsedSecs(Zend_Db_Profi1 er::INSERT). " Sekunden"; echo "<br>- Dauer der SELECTs: "; echo $profiler->getTotalElapsedSecs(Zend_Db_Profi1er::SELECT). " Sekunden"; Listing 2.32 Nutzung des Profilers Dieser Code erzeugt die Darstellung, die Sie in Abbildung 2.7 sehen. C> n n http://127.O.O.l/.../zf-buch/Db/7.php CD » Gesamtzahl der Abfragen: 2000 - Anzahcl der INSERTs: 1OOO - Anzahcl der SELECTs: 1000 Dauer aller Abfragen: 0.677295207977 Sekunden - Dauer der INSERTs: 0273243188858 Sekunden - Dauer der SELECts; 0.404052019119 Sekunden — Abbildung 2.7 Daten, die der Profiler ermittelt hat 111
e-bol.net 2 | Datenbankzugriff mit Zend_Db Die Methode getTotal NumQueries() liefert Ihnen die Gesamtzahl der Anfragen zurück, die protokolliert wurden. Wie bei den meisten anderen Methoden auch können Sie hier einen Parameter übergeben, der dafür sorgt, dass Sie genauere Daten erhalten. Das heißt, Sie können mit der Konstante Zend_Db_Prof i - ler:: INSERT spezifizieren, dass Sie nur Daten zu den INSERT-Statements auslesen wollen, die ausgeführt wurden. Weitere Konstanten sind: Zend_Db_Prof i - ler::SELECT, Zend_Db_Profiler::CONNECT, Zend_Db_Profi 1 er::DELETE, Zend_ Db_Profi 1 er: :TRANSACTION und Zend_Db_Profi 1 er::UPDATE. Die meisten Namen sind hier sicher selbsterklärend. Mit TRANSACTION rufen Sie Informatio- nen zu den Punkten ab, die eine Transaktion betreffen, also COMMIT etc. Interes- sant ist noch die Konstante Zend_Db_Profiler::QUERY, mit der Sie Informatio- nen über alle Queiy-Typen abrufen können, die durch die bereits genannten Konstanten noch nicht abgedeckt sind. Die Methode getTotal El apsedSecs() teilt Ihnen mit, wie lange die Ausführung der Abfragen gedauert hat. Auch hier können Sie wieder die Konstanten nutzen, um genauer spezifizierte Informationen zu erhalten. Damit erhalten Sie schon einen schnellen Überblick. Allerdings sind diese Daten noch recht allgemein. Gerade wenn Sie ein umfangreiches Script analysieren wollen, sind die Informationen oft zu allgemein. Der Profiler protokolliert aber die Daten zu jeder Abfrage einzeln. Um Daten zu einer Abfrage zu erhalten, kön- nen Sie die Methode getQueryProfi1e() nutzen, welche die ID einer Abfrage übergeben bekommt. ID heißt an dieser Stelle, dass die Abfragen einfach der Reihe nach, beginnend mit 0, durchnummeriert werden. In den meisten Fällen wird man vermutlich nicht unbedingt wissen, welche der Abfragen man genauer analysieren sollte. Daher können Sie auch alle Profile auf einmal auslesen, indem Sie die Methode getQueryProfi 1 es () aufrufen. Bei dieser Methode können Sie auch die bereits erwähnten Konstanten nutzen, um nur SELECT-Satements, UPDATE-Statements o.Ä. auszulesen. Interessant in diesem Zusammenhang ist, dass Sie diese Konstanten auch mit einem Bit-Oder (|) kombinieren können. Sollte Sie nur die letzte Abfrage interessieren, dann ist getLastQueryProfi 1 e () Ihr Freund. Mit diesen Methoden erhalten Sie ein Objekt der Klasse Zend_Db_Profi 1 er_ Query bzw. bei getQueryProfi1es() ein Array mit entsprechenden Objekten. Die folgenden Zeilen lesen die Informationen zu einer bestimmten Abfrage aus: // Query auslesen Squery = $profiler->getQueryProfile( 1004); // Abfrage echo "<br>Query: ".$query->getQuery(); // Parameter bei prepared Statements 112
e-bol.net Performanceanalyse mit Zend_Db_Profiler | 2.4 echo "<br>Parameter: <pre>"; print_r($query->getQueryParams()): echo "</pre>"; // Typ der Abfrage echo "<br>AbfrageTyp: ".$query->getQueryType(); // Laufzeit des Statements echo "<br>Laufzeit: ".$query->getElapsedSecs(); Die Methode getQuery() liest die Abfrage aus und gibt sie als String zurück. Hierbei bleiben auch die Platzhalter in der Abfrage erhalten. Die in der Abfrage genutzten Parameter gibt Ihnen die Methode getQueryParamsO in Form eines Arrays zurück. Der Typ der Abfrage wird mit der Methode getQueryTypet) ermittelt. Sie liefert Ihnen einen Integer-Wert zurück. Diesen können Sie mit den Zend_Db_Profi 1 er-Konstanten vergleichen. Dieses Beispiel liefert den Wert 32 zurück, was Zend_Db_Profi 1 er: :SELECT entspricht. Wie lange der Datenbank- server für die Ausführung des Statements benötigt hat, gibt die Methode get- E1 apsedSecs() an. Die Ausgabe, die von diesen Zeilen generiert wird, sehen Sie in Abbildung 2.8. n http://127.0.0.l/~carsten/zf-buch/Db/7.php G3 Query: SELECT * FROM plz WHERE id = ? OR id = ? Parameter Array ( [1] -> 55 (2) -> 729 AbfirageTyp: 32 Laufzeit: 0.000493049621582 Abbildung 2.8 Informationen zu einer einzelnen Query Diese Funktionalitäten liefern Ihnen ein mächtiges Mittel, um das Performance- Verhalten Ihrer Anwendung zu analysieren und zu verbessern. Allerdings kann die Menge der Informationen schnell sehr groß werden. Da Sie in den meisten Fällen sicher nur Informationen über die Statements haben möchten, die eine höhere Laufzeit aufweisen, können Sie daher auch hinsichtlich der Laufzeit fil- tern. Mit der Methode setFi 1 terEl apsedSecs(5) legen Sie fest, dass nur SQL- Statements protokolliert werden, die eine Laufzeit von mindestens fünf Sekun- den gehabt haben. Übergeben Sie der Methode den Wert nul 1, so wird der Filter wieder gelöscht. 113
e-bol.net
e-bol.net Die Wahrheit ist selten so oder so. Meist ist sie so oder so. - Geraldine Chaplin 3 Benutzer- und Rechtemanagement Die meisten Anwendungen, die heutzutage entwickelt werden, benötigen zumindest die Möglichkeit, bestimmte Bereiche mit einem Passwort zu sichern. In vielen Fällen müssen die Möglichkeiten auch deutlich ausgefeilter sein, und es muss unterschiedliche Rechte für verschiedene Benutzergruppen geben. In die- sem Kapitel finden Sie alles, was Sie dazu benötigen, inklusive der Verwaltung von Sessions mithilfe von Zend_Session. 3.1 Rechteverwaltung mit Zend_Acl Sicher kennen Sie das folgende Problem: Sie wollen eine Webanwendung erstel- len und bestimmte Funktionalitäten mit Passwörten sichern. Falls es nur ganz all- gemein einen oder mehrere Administratoren, die alle die gleichen Rechte haben, gibt, ist dies unproblematisch. Wollen Sie nun aber mehrere Benutzergruppen einrichten, die unterschiedlich viele Rechte haben, so wird dies schnell aufwän- dig. Gehen wir davon aus, dass Sie eine kleine Online-Zeitung erstellen wollen. Es soll »einfache« Leser geben sowie Leser, die sich registriert haben und somit kommentieren dürfen. Darüber hinaus soll es Redakteure geben, die neue Texte erstellen dürfen. Zu guter Letzt gibt es natürlich noch einen Administrator, der allerdings nicht zur eigentlichen Redaktion gehört und somit keine Artikel verfas- sen darf. Auf den ersten Blick ist das schnell verwirrend, wie ich finde. Aber las- sen Sie uns das Ganze ein wenig strukturierter angehen. Der einfache Leser darf nur lesen, er hat also die wenigsten Rechte. Ein registrier- ter Leser darf kommentieren, aber natürlich auch lesen. Das heißt, er hat mehr Rechte. Man könnte auch einfach sagen, dass er die Rechte des einfachen Lesers hat bzw. erbt und noch eigene Rechte hinzu bekommt. Ein Redakteur wiederum darf auch lesen und kommentieren und darf darüber hinaus Artikel schreiben. Ähnlich verhält es sich bei dem Administrator. Dieser darf auch lesen und kom- 115
e-bol.net 3 | Benutzer- und Rechtemanagement mentieren. Er erbt somit die Rechte des registrieren Benutzers, allerdings darf er selbst keine Artikel schreiben. Somit erbt er nicht die Rechte des Redakteurs. In diesem kleinen Beispiel haben wir es mit verschiedenen Gruppen zu tun. Sol- che Gruppen werden in der Anwendungsentwicklung gerne als Rollen bezeich- net. Etwas abstrakter formuliert ist eine Rolle ein Objekt, das etwas anfordert. Das was angefordert wird, sind die Rechte, von denen in dem Beispiel die Rede war. Diese werden in diesem Zusammenhang auch gerne als Ressourcen bezeich- net. Mit anderen Worten: Eine Rolle fordert eine Ressource an. Eine Rolle kann ein einzelner Benutzer oder eben auch eine Gruppe von Benutzern sein, sofern Sie die Benutzer der Rolle entsprechend zuordnen. Eine Ressource muss nicht immer nur ein Recht im eigentlichen Sinn sein, sondern kann beispielsweise auch die Nutzung einer echten Ressource (wie der Zugriff auf eine Datenbank) darstellen. Mit Zend_Acl, das ACL steht übrigens für Access Control List, können Sie ein sol- ches Rechtesystem schnell und einfach aufbauen. Nachdem Sie ein ACL-Objekt abgeleitet haben, können Sie in diesem Rollen anlegen. Die Klasse sieht hierbei keine Unterscheidung vor, ob es sich bei den Rollen um einzelne Benutzer oder um Gruppen handelt. Das ist im Endeffekt eine Frage der Implementation, die Ihnen überlassen bleibt. Nachdem die Rollen angelegt sind, werden Ressourcen angelegt und schließlich die Rechte der Rollen zu den Ressourcen gesetzt. Bezogen auf das obige Beispiel könnte der Aufbau des Rechtesystems so aussehen: // Benötigte Klassen inkludieren require_once 'Zend/Acl.php'; requi re_once 'Zend/Acl/Ro1e.php’; requi re_once 'Zend/Acl/Resource.php'; // Neue Access Control List ableiten $acl = new Zend_Acl(): // einfachen Leser anlegen $acl->addRole(new Zend_Acl_Role(’leser’)); // Registrierten User anlegen: er erbt von leser $acl->addRole(new Zend_Acl_Role('reg_leser'),1leser’); // Redakteur anlegen: er erbt von leser und reg_leser $acl->addRole(new Zend_Acl_Role('redakteur'),'reg_leser’); // Ressourcen anlegen $acl->add(new Zend_Acl_Resource('lesen')); $acl->add(new Zend_Acl_Resource('kommentieren')); 116
e-bol.net Rechteverwaltung mit Zend_Acl | 3-1 $acl->add(new Zend_Acl_Resource('artikel_schreiben')); // Rechte vergeben $acl->allow('lesen', 'lesen'); $acl->allow(’reg_leser', 'kommentieren'); $acl->allow(’redakteur', 'artikel_schreiben’); // Rechte abfragen if (true === $acl->isAllowed('leser', 'lesen')) { echo "Ja, 'lesen' darf lesen<br>"; } if (true === $acl->isAllowed('leser', 'kommentieren')) { echo "Ja, 'lesen' darf kommentieren<br>"; } el se { echo "Nein, 'lesen' darf nicht kommentieren<br>"; } if (true === $acl->isAllowed('redakteur', 'kommentieren')) { echo "Ja, 'redakteur' darf kommentieren<br>"; } Listing 3.1 Erstellen einer ACL Nachdem das neue ACL-Objekt abgeleitet wurde, können die einzelnen Rollen mithilfe von addRol e() angemeldet werden. Eine Rolle besteht in diesem Fall aus einem Zend_Acl_Role-Objekt. Der Konstruktor der Klasse Zend_Acl_Role bekommt als Parameter den Namen der Rolle übergeben. Genau genommen han- delt es sich dabei nicht um den Namen, sondern um eine eindeutige ID. So kön- nen Sie also auch eine Zahl, wie beispielsweise eine ID aus einer Datenbank, nut- zen. Wenn Sie ein solches Objekt einmal abgeleitet haben, können Sie den Namen bzw. die ID auch jederzeit mithilfe der Methode getRol eld() wieder aus dem Objekt auslesen. Die Methode addRol e() unterstützt auch einen zweiten Parameter, wie Sie bei dem zweiten und dritten Aufruf sehen. An zweiter Stelle können Sie der Methode die Information übergeben, von welcher Rolle oder von welchen Rol- len die neue Rolle Rechte erben soll. Das heißt, Sie können hier auch ein Array 117
e-bol.net 3 | Benutzer- und Rechtemanagement mit Namen von Rollen übergeben. Anstelle der Namen können Sie alternativ auch Rollen-Objekte übergeben. Das Anlegen der Ressourcen erfolgt auf einem ähnlichen Weg. Zunächst wird eine Ressource generiert, in diesem Fall wird also ein Objekt der Klasse Zend_ Acl_Resource abgeleitet. Auch hier erhält der Konstruktor wieder einen Namen bzw. eine ID übergeben. Das so generierte Objekt wird an die Methode add() übergeben, die es dann in der Ressourcenliste einträgt. Übrigens ist es auch hier möglich, eine oder mehrere übergeordnete Ressourcen als zweiten Parameter zu übergeben. Mit der Methode al 1 ow() wird schließlich festgelegt, welche Rolle Zugriff auf welche Ressource hat. Mithilfe der Methode deny() können Sie übrigens auch explizit den Zugriff auf eine Ressource unterbinden. Im Normalfall ist das nicht nötig, kann bei der Vererbung von Rechten aber einen deutlichen Unterschied machen, wie Sie in Abschnitt 3.1.1 nachlesen können. Falls Sie mit al 1 ow() und deny() arbeiten, sollten Sie im Hinterkopf behalten, dass die beiden Methoden sich gegenseitig überschreiben. Wenn Sie also einen Zugriff mit deny() verboten haben, so können Sie ihn danach wieder mit al 1 ow() gewähren. Mithilfe von 1 sAl 1 owed() kann schließlich geprüft werden, ob eine Rolle Zugriff auf eine bestimmte Ressource hat. Vergeben Sie keine Zugriffs rechte, so geht das Paket standardmäßig davon aus, dass das Recht nicht gewährt wurde. Hierbei müssen Sie bitte beachten, dass sowohl die Ressource als auch die Rolle angelegt sein müssen, da sonst die Abfrage in einer Exception resultiert. Die Ausgabe von Listing 3.1 sehen Sie in Abbildung 3.1. ODO http://127.0.0.1/~ca . en/zf-buch/acl/l.php 0 Q Google » □□ Planet PHP (122) Apple (19)t Amazon eBay » Ja, ’lcscr1 darf lesen Nein, ’lcscr* darf nicht kommentieren Ja, 'redaktcuf darf kommentieren Abbildung 3.1 Ausgabe der Benutzerverwaltung 3.1.1 Vererbung von Rechten Im vorherigen Kapitel haben Sie schon ein kleines Beispiel gesehen, wie eine Ver- erbung von Rechten aussehen kann. Allerdings ist das in einigen Fällen ein wenig trickreich, wie die folgenden Beispiele zeigen. Das erste Beispiel verhält sich so, 118
e-bol.net Rechteverwaltung mit Zend_Acl | 3-1 wie Sie es erwarten. Die Rollen redakteur und redakteur_2 haben beide das Recht zu kommentieren: $acl->addRole(new Zend_Acl_Role('leser')); $acl->addRole(new Zend_Acl_Role('reg_leser'), 'leser'); $acl->add(new Zend_Acl_Resource('lesen')); $acl->add(new Zend_Acl_Resource('kommentieren')); $acl->allow('leser' , 'lesen'); $acl->al1ow(’reg_leser', 'kommentieren'); $parents_redakteur = array ('reg_leser', 'leser'); $acl->addRole(new Zend_Acl_Role('redakteur'),$parents_redakteur); if (true === $acl->isAl1owed('redakteur','kommentieren')) { echo "'redakteur' darf kommentieren"; } $parents_redakteur = array ('leser', 'reg_leser’); $acl->addRole(new Zend_Acl_Role('redakteur_2'), $parents_redakteur); if (true === $acl->isAl1owed('redakteur_2',’kommentieren')) { echo "’redakteur_2' darf kommentieren"; } Im nächsten Beispiel wurde nur eine Zeile ergänzt. Hier wurde der Rolle leser das Recht zu kommentieren explizit entzogen, was keinen großen Unterschied machen sollte, da ein Recht, das nicht explizit zugestanden wurde, als nicht gewährt gilt. // Anlegen der Ressourcen und Rollen wie vorher $acl->allow('leser' , 'lesen'); $acl->deny('leser', ’kommentieren'); $acl - >al1ow(’reg_leser', 'kommentieren'); $parents_redakteur = array ('reg_leser', 'leser'); $acl->addRole(new Zend_Acl_Role('redakteur'),$parents_redakteur); if (true === $acl->isAl1owed('redakteur','kommentieren')) { echo "'redakteur' darf kommentieren"; } $parents_redakteur = array ('leser', 'reg_leser’); 119
e-bol.net 3 | Benutzer- und Rechtemanagement $acl->addRole(new Zend_Acl_Role('redakteur_2'), $parents_redakteur); if (true === $acl->isAl1owed('redakteur_2',’kommentieren')) { echo "’redakteur_2' darf kommentieren"; } // Ausgabe: // 'redakteur_2' darf kommentieren In diesem Beispiel hat die Rolle redakteur kein Recht mehr, auf die Ressource kom- mentieren zuzugreifen, obgleich redakteur und redakteur_2 augenscheinlich gleich angelegt sind. Dieses Verhalten resultiert aus einer Besonderheit von Zend_ Acl. Und zwar werden die übergeordneten Gruppen analysiert, und es wird geprüft, ob für die fragliche Ressource ein Recht gesetzt ist. Solange kein entspre- chender Eintrag gefunden wurde, wird der nächste Eintrag abgearbeitet. Das heißt: Falls nicht explizit eine Information zu einer Ressource gefunden wird, wird die nächste Rolle analysiert. Sobald eine Information gefunden wird, wird die Bearbei- tung abgebrochen und die gefundene Information zurückgegeben. Da die überge- ordneten Rollen aber von hinten nach vorne analysiert werden, wird bei der Rolle redakteur erst die Rolle leser analysiert. Bei dieser ist im zweiten Beispiel ein explizites Verbot enthalten, das in der ersten Variante noch nicht vorhanden war. Das Verbot wird gefunden und die Verarbeitung abgebrochen. Möchten Sie tat- sächlich mit Vererbung arbeiten, so sollten Sie dieses Verhalten immer beachten. 3.1.2 Verfeinern des Rechtesystems In den bisherigen Beispielen wurden immer Rollen genutzt, denen der komplette Zugriff auf eine Ressource gestattet oder entzogen wurde. Möchten Sie ein kom- plexeres Rechtesystem entwerfen, so wird diese Vorgehensweise aber schnell sehr aufwändig. Daher können Sie das System noch verfeinern. Und zwar kön- nen Sie für jede Ressource noch explizit Rechte vergeben: // Anlegen der Ressourcen und Rollen // leser dürfen Artikel und Kommentare // lesen aber nicht schreiben $acl->allow('leser' , ’artikel', 'lesen'); $acl->allow('leser' , 'kommentar', 'lesen'); $acl->deny('1eser', 'kommentar', 'schreiben'); // reg_leser erbt von leser, darf Kommentare aber auch schreiben $acl->al1ow(’reg_leser', 'kommentar', array ('lesen', 'schreiben')); 120
e-bol.net Rechteverwaltung mit Zend_Acl | 3-1 if (true === $acl->isAllowed('reg_leserkommentar’)) { // Wird nicht ausgegeben echo "'reg_leser' darf etwas mit Kommentaren machen"; } if (true === $acl->iSAUowed('reg_leser', 'kommentar’, *schreiben’)) { // Wird ausgegeben echo "'reg_leser' darf Kommentare schreiben"; } Listing 3.2 Verfeinern der Zugriffsrechte In diesem Beispiel werden für die Ressourcen artikel und kommentar explizit Rechte vergeben. Die Rolle l eser darf Artikel und Kommentare lesen, wohinge- gen die Rolle reg_user auch das Recht schreiben auf die Ressource kommentar hat. Die Rechte werden also einfach als weiterer Parameter in Form eines Strings oder Arrays an die Methode al l ow() bzw. denyO übergeben. Auch bei der Abfrage, ob eine Rolle ein bestimmtes Recht hat, muss neben der gewünschten Ressource noch das fragliche Recht mit angegeben werden. Bei der ersten Abfrage mithilfe von isAllowed() wurde dieser dritte Parameter nicht überge- ben, was dazu führte, dass die Methode fal se zurückgab. Die Nutzung von Rechten vereinfacht zwar schon vieles, aber an einigen Stellen wird das sicher noch unzureichend sein. In vielen Fällen werden Sie noch das Problem haben, dass das Gewähren eines Rechts von bestimmten zusätzlichen Faktoren abhängt. So könnte es beispielsweise sinnvoll sein, dass ein Redakteur nachts in der Zeit zwischen 2 und 4 Uhr keine Artikel schreiben darf, da zu dem Zeitpunkt gerade ein Backup der Daten durchgeführt wird. Auch solche Szenarien können ohne Probleme abgebildet werden. Dazu wird mit einer sogenannten Assertion, einer Annahme, gearbeitet. Eine solche Assertion wird in Form einer eigenen Klasse implementiert, in der es eine Methode namens assertO geben muss. Diese Klasse muss das Interface Zend_Acl_ Assert_Interface implementieren. Ein Objekt dieser Klasse wird dann als letz- ter Parameter, also nach Rolle, Ressource und Recht, an die Methode allow() übergeben: $acl->allow(’redakteur’, 'artikel*, 'schreiben', new Uhrzei tCheck()); 121
e-bol.net 3 | Benutzer- und Rechtemanagement Bei jedem Test, ob eine Ressource von einer Rolle genutzt werden darf, wird auch die Methode assert() in dem Objekt aufgerufen. Die Methode muss das ACL-Objekt, ein Rollen-Objekt, ein Ressourcen-Objekt sowie ein Recht als Para- meter akzeptieren, wobei das Recht ein optionaler Parameter ist, da es ja nicht immer genutzt wird. Da die Klassen für das Rollen- und das Ressourcen-Objekt jeweils Interfaces implementieren, ist es sinnvoll, hier mit Typehinting zu arbei- ten. Die Methode assertO sollte immer dann true zurückgeben, wenn »aus Sicht der Methode« nichts dagegen spricht, der Rolle den Zugriff auf die Res- source zu gewähren. Eine solche Klasse könnte beispielsweise wie folgt imple- mentiert werden: dass UhrzeitCheck Implements Zend_Acl_Assert_Interface { public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $role = null, Zend_Acl_Resource_Interface $resource = null, $pri vi1 ege = nul1) { // Nur zur Sicherheit... // Wurde die Methode mit der passenden Ressource // und dem passenden Recht aufgerufen? if (’artikel’ 1= $resource->getResource!d() || 'schreiben' != $privilege) return true: 1 // Uhrzeit auslesen $stunde = date( 'M'); // Innerhalb der "verbotenen Zeit"? if ($stunde >= 2 && $stunde <= 4) { // Dann geben wir false zurueck return false: 1 el se { // Sonst ist alles OK und wir geben true zurück return true: } } Listing 3.3 Erstellen einer Assertion Auch der Methode deny () können Sie ein solches Objekt übergeben. Momentan ist es allerdings noch so, dass der Rückgabewert der Methode assertO hierbei 122
e-bol.net Rechteverwaltung mit Zend_Acl | 3-1 keinen Einfluss auf das Verhalten von deny() hat, was ja auch Sinn hat, da etwas, das verboten ist, immer verboten sein sollte, um kein Sicherheitsleck zu provo- zieren. Interessant ist übrigens auch die Möglichkeit, dass Sie das Verhalten der Access Control List global mithilfe einer Assertion steuern können. Wenn Sie in der Zeit zwischen 2 und 4 Uhr alle Aktionen unterbinden wollen, so könnten Sie das so implementieren: $acl->allow(null, null, null, new UhrzeitCheck()): Dazu müssten Sie in der Assertion allerdings die Abfrage der übergebenen Para- meter entfernen. 3.1.3 Manipulieren von Rechten Die Rechtevergabe, wie Sie sie bisher kennengelernt haben, ist für viele Fälle aus- reichend. Die Initialisierung der Access Control Liste können Sie sicher in den meisten Fällen in das Bootstrap-File integrieren. Sollten Sie allerdings ein sehr komplexes Rechtesystem nutzen, so könnte es vorteilhaft sein, das Zend_Acl- Objekt im Dateisystem oder in einer Session zu speichern. In diesem Fall können Sie es einfach serialisieren und speichern. Soll ein ACL-Objekt allerdings über einen längeren Zeitraum genutzt werden, so kann es passieren, dass Sie die dort enthaltenen Rechte ändern müssen. Möchten Sie eine oder mehrere Ressourcen entfernen, so helfen remove() und removeAl l () Ihnen weiter. Die erste Methode erhält eine bestimmte Ressource als Parameter übergeben, die dann zusammen mit allen eventuell vorhandenen Kind-Ressourcen entfernt wird. Die zweite Methode eliminiert alle vorhandenen Ressourcen auf einmal. Das funktioniert natürlich ebenso mit Rollen. Diese kön- nen Sie mit removeRole() einzeln oder mit removeRol eAl l () alle auf einmal ent- fernen. Um einzelne Rechte oder Verbote zu entfernen, sind die Methoden r emo - veAllowO und removeDeny() deklariert. Beide Methoden akzeptieren bzw. benötigen die gleichen Parameter wie die Methoden al low() bzw. deny(), mit denen die Rechte im Vorfeld vergeben wurden: // Vergabe des Rechts $acl->allow('leser', ’artikel', 'lesen'): // Hier ist natürlich noch ganz viel Code dazwischen // Entziehen des Rechts $acl->removeAllow('leser',’artikel' , 'lesen'); In solchen Zusammenhängen kann es hilfreich sein herauszufinden, ob bestimmte Rollen oder Ressourcen im ACL bekannt sind. Für solche Abfragen 123
e-bol.net 3 | Benutzer- und Rechtemanagement können Sie auf die Methoden has() und hasRole() zurückgreifen. Die erste bekommt eine Ressource übergeben und bestätigt das Vorhandensein derselben mit true bzw. liefert false zurück, um Ihnen mitzuteilen, dass die Ressource nicht bekannt ist. Die Methode hasRole() bietet die gleiche Funktionalität für Rollen. Um festzustellen, ob Ressourcen oder Rollen Rechte an Kinder vererbt haben, sind die Methoden inheritsO und inheritsRole() vorgesehen. Auch hier ist die erste für Ressourcen und die zweite für Rollen gedacht. Beide bekommen zuerst das potenzielle Kind-Element und dann das eventuelle Eltern-Element übergeben. In beiden Fällen können Sie die Elemente als Objekte oder die Namen als String übergeben. Mit einem booleschen Wert, den Sie an dritter Stelle übergeben, legen Sie fest, ob eine direkte Eltern-Kind-Beziehung geprüft werden soll, oder ob das Kind sich nur in dem Teilbaum unter dem Eltern- bzw. Vorfahren-Objekt befinden soll. 3.2 Benutzerauthentifikation mit Zend_Auth Die Klasse Zend_Auth ermöglicht Ihnen die Authentifikation von Benutzern. Das bedeutet, dass Sie anhand eines Benutzernamens und eines Passwortes erken- nen, ob ein Benutzer berechtigt ist, Zugriff auf eine bestimmte Ressource zu erhalten. Es handelt sich also nicht um eine Rechteverwaltung mit verschiedenen Zugriffsrechten. Dafür ist Zend_Acl zuständig. Zend_Auth kennt standardmäßig die Möglichkeit, Benutzerdaten gegen eine Datenbank oder gegen Dateien zu authentifizieren. 3.2.1 Datenbankbasierte Authentifikation Möchten Sie den Benutzernamen und das Passwort, die ein Benutzer eingegeben hat, mit einem Datenbankinhalt vergleichen, so benötigen Sie hierfür zuerst eine Tabelle. Zend_Auth verlangt dabei nicht, dass eine bestimmte Tabellenstruktur eingehalten wird. Sie können die Tabelle also selbst entwerfen. Für die hier auf- geführten Beispiele wurde die folgende Tabelle genutzt: CREATE TABLE 'zend_user' ( 'id' int(ll) NOT NULL AUTO_INCREMENT, 'username' varchar(20) NOT NULL, 'Passwort' varchar(32) NOT NULL, 'bemerkungen' varchar(255) DEFAULT NULL, PRIMARY KEY ('id' ). 124
e-bol.net Benutzerauthentifikation mit Zend_Auth | 3-2 UNIQUE KEY 'username' ('username') ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Der Zugriff auf die Tabelle erfolgt über eine der Adapter-Klassen, die Zend_Db zur Verfügung stellt. Da ich hier mit MySQL arbeite, habe ich den Pdo_Mysql-Adap- ter (Klasse Zend_Db_Adapter_Pdo_Mysql) genutzt. Das Datenbankobjekt wird dann an den Konstruktor der Klasse Zend_Auth_Adapter_DbTabl e übergeben. Zend_Auth benötigt aber auch die Information, wie die Tabelle in der Datenbank heißt und wie die Spalten für den Benutzernamen und das Passwort lauten. Diese Angaben können Sie direkt an den Konstruktor übergeben. Alternativ können Sie die Methoden setTableName(), setldentityCol umn() und setCredentialCo- 1 umn() nutzen, um die Namen der Tabelle der Benutzernamen- bzw. Passwort- Spalte zu übergeben. Danach können Sie auch schon loslegen und Benutzer authentifizieren. Der Benutzername muss dabei mit der Methode setldentity() an die Klassenin- stanz übergeben werden; das Passwort wird mithilfe von setCredenti al () zuge- wiesen. Nachdem dies erfolgt ist, können Sie authenticate() aufrufen, womit die Benutzerinformationen gegen die Datenbank geprüft werden: // Klassen einbinden require_once ’Zend/Db. php’; require_once ’Zend/Auth/Adapter/DbTable.php’; // Datenbankzugriff konfigurieren $opts = array('host’ => ’localhost’, 'username' => ’root', 'password’ . 'dbname' => 'test'): // Datenbankadapter ableiten $db = Zend_Db::factory('Pdo_Mysql',$opts): // Benötigte Auth-Klasse ableiten $auth = new Zend_Auth_Adapter_DbTable($db): // Namen der Tabelle und Namen der Spalten setzen $auth->setTableName(’zend_user') ->setldenti tyColumn('username') ->setCredenti alColumn(’passwort'); if (true — empty ($_POST['username']) || true — empty ($_POST['passwort'])) ( 125
e-bol.net 3 | Benutzer- und Rechtemanagement // Loginformular ausgeben echo "<form method=’post'>"; echo "<table>”; echo "<tr> <td>Username</td> CtdXinput type=’text' name='username'>C/td> C/tr>"; echo "<tr> <td>Passwort</td> CtdXinput type=’password' name=’Passwort1>C/td> C/tr>"; echo "<tr> <td colspan=’2' align='center'> Cinput type='submit' value='Einloggen'> </td> C/tr>"; echo "</table>": } el se { // Daten an Objekt übergeben Sauth->setldenti ty($_POST[’username']) ->setCredenti al($_POST['passwort*]) ->setCredenti alTreatment('MD5(?)'); // Authentifikation durchführen Sresult = $auth->authenticate(); // Rückgabecode auslesen Scode = Sresult->getCode(); // Rückgabewert auswerten switch (Scode) { case Zend_Auth_Result::FAILURE: Stehler = 'Unbekannter Fehler'; break; case Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND : Stehler = 'Username unbekannt': break; case Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS : Stehler = 'Username nicht eindeutig’; break; 126
e-bol.net Benutzerauthentifikation mit Zend_Auth | 3-2 case Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID : Sfehler = 'Falsches Passwort break; case Zend_Auth_Result::FAILURE_UNCATEGORIZED : Stehler = 'Nicht kategorisierter Fehler’; break; // Haben wir eine Fehlermeldung? if (isset (Stehler)) { echo Stehler; d 1 e (); echo "Sie sind eingeloggt"; } Listing 3.4 Authentifikation gegen eine Datenbanktabelle In diesem Listing wird entweder ein Formular ausgegeben, oder die Daten, die in das Formular eingegeben wurden, werden mit den Benutzern verglichen, die in der Datenbank angelegt sind. Bei der Übergabe der Benutzerdaten an das Objekt wird zusätzlich noch die Methode setCredentialTreatment() aufgerufen. Mit ihr legen Sie fest, ob das eingegebene Passwort im Klartext oder verschlüsselt mit der Datenbankspalte verglichen werden soll. Wenn Sie diese Methode also nicht nutzen, wird das Passwort so mit der Datenbankspalte verglichen, wie es einge- geben wurde. Da Sie Passwörter wenn möglich immer verschlüsselt in der Daten- bank ablegen sollten, können Sie mit dieser Methode definieren, welcher Ver- schlüsselungsalgorithmus genutzt werden soll. Sie können hier beispielsweise MD5( ?) oder PASSW0RD( ?) angeben. Die Fragezeichen werden dann jeweils durch das eingegebene Passwort ersetzt, wenn die Daten an die Datenbank geschickt werden. Der übergebene String wird nicht daraufhin geprüft, ob er eine der bei- den Funktionen enthält. Sie können also auch eine andere Funktion ansprechen. Wichtig ist nur, dass das Fragezeichen enthalten sein muss. Diesen String könn- ten Sie auch als fünften Parameter an den Konstruktor übergeben. Die Methode authenticate() liefert ein Objekt der Klasse Zend_Auth_Resul t zurück. In diesem Objekt ist ein Code enthalten, der Ihnen mitteilt, ob der Benut- zer erfolgreich angemeldet werden konnte. Den Code können Sie mit den Kon- stanten aus Tabelle 3.1 vergleichen. 127
e-bol.net 3 | Benutzer- und Rechtemanagement Konstante Bedeutung FAILURE unbekannter Fehler FAILURE_IDENTITY_NOT_FOUND Das Log-in wurde nicht gefunden. FAILURE_IDENTITY_AMBIGUOUS Der Benutzer ist mehrfach vorhanden. (Achtung: Die Log-ins sind nicht case-sensitive.) FAILURE_CREDENTIAL_INVALID Das Passwort ist ungültig. FAILUREJJNCATEGORIZED unkategorisierter Fehler SUCCESS Das Log-in war erfolgreich. Tabelle 3.1 Rückgabewerte bei der Authentifikation 3.2.2 Dateibasierte HTTP-Authentifikation Wie eingangs erwähnt, können Sie Benutzerdaten auch mit Benutzerinformatio- nen vergleichen, die in einer Datei hinterlegt sind. Das Dateiformat ist so aufgebaut, dass eine Zeile eine Zugangsberechtigung ent- hält. Die Zeile wird mit dem Benutzernamen eingeleitet, dann folgt »Realm«, also derjenige Bereich, zu dem der Benutzer Zugang haben soll. Dann schließt sich das Passwort an. Die drei Teile sind jeweils durch einen Doppelpunkt voneinander getrennt. Ein Benutzername darf in einer Datei durchaus mehrfach auftauchen, allerdings muss die Kombination aus Bereichsangabe und Passwort eindeutig sein. Somit können Sie auch bei größeren Projekten mit nur einer Passwort-Datei arbeiten und damit unterschiedliche Bereiche absichern. Das Paket Zend_Auth unterstützt zwei Authentifikationsverfahren: Basic und Digest. Hierbei gilt, dass die Passwörter in Abhängigkeit vom Authentifikations- verfahren unterschiedlich abgelegt werden müssen. Bei einer Datei für das ältere Basic-Verfahren muss das Passwort Base64 codiert sein, wohingegen bei dem neueren Digest-Verfahren eine MD5-Verschlüsselung genutzt wird. Um einen Benutzer in einer Datei für das Digest-Verfahren anzulegen, könnten Sie bei- spielsweise die folgenden Zeilen verwenden: $user = 'carsten ’; Srealm = 'Mein privates Verzeichnis': Spassword = ’total geheim’; $fp = fopen('userliste.txt',’a'); Szeile = "$user:$realm:".md5($password)."\n"; fputs($fp, Szeile): fclose($fp): Listing 3.5 Einfaches Beispiel zum Anlegen von Benutzern 128
e-bol.net Session-Verwaltung mithilfe von Zend_Session | 3-3 Für das Basic-Verfahren müssten Sie den Aufruf von md5() nur durch den Aufruf von base64_encode() ersetzen. Wenn Sie die Datei über den Webserver anlegen und/oder verwalten, vergessen Sie bitte nicht, dass der Webserver Leserechte auf die Datei hat. Ein Angreifer könnte die Datei also direkt aufrufen, sofern er den Pfad kennt. Bitte stellen Sie daher sicher, dass die Datei an einem Ort abgelegt wird, an dem der Webserver nicht direkt lesen kann. Abschließend noch eine Anmerkung. Falls Sie Zend_Auth in einer MVC-Anwen- dung nutzen wollen, so wirft das die Frage auf, an welcher Stelle Sie die Authen- tifikation ausführen. Hierbei bietet es sich an, ein Plug-in wie beispielsweise pre- Di spatchf ) zu benutzen. 3.3 Session-Verwaltung mithilfe von Zend_Session Mithilfe der Klasse Zend_Session können Sie auf einfache Art und Weise Sessi- ons verwalten. Auch wenn die native Session-Verwaltung von PHP durchaus ein- fach zu handhaben und zuverlässig ist, so bringt sie doch ein paar kleinere Pro- bleme mit sich. Die aus meiner Sicht größte Einschränkung ist sicherlich, dass PHP die Session-Daten standardmäßig im temporären Verzeichnis des Servers ablegt. Somit ist die Session stets nur auf diesem einen Server vorhanden, und der Benutzer kann nicht einfach auf einen anderen Server wechseln, ohne die Session-Daten zu verlieren. Gerade bei umfangreichen Anwendungen, die über mehrere Server verteilt sind, kann das unvorteilhaft sein, insbesondere dann, wenn die Benutzer-Log-ins ebenfalls über Sessions verwaltet werden. Diese Probleme merzt Zend_Session aus. Darüber hinaus stellt sie noch ein sau- ber strukturiertes, objektorientiertes Interface zur Verfügung. Ein wenig gewöhnungsbedürftig ist vielleicht die Tatsache, dass Zend_Session auf Basis von Namensräumen arbeitet. Die Idee hinter diesen Namespaces ist ein- fach: In einer komplexen Anwendung müssen unter Umständen viele Daten in Sessions abgelegt werden. Nutzen die Klassen und der restliche Code alle den sel- ben Speicherbereich, so kann es schnell passieren, dass Variablen in der Session den selben Namen haben und die Werte überschrieben werden. Zend_Session verfolgt den Ansatz, dass jeder Teil der Anwendung einen eigenen Namensraum anlegen kann. Dieser Namensraum ist getrennt von den anderen Namensräu- men, sodass kein Wert überschrieben werden kann, selbst dann nicht, wenn die Bezeichner bzw. Variablen identisch sind. 129
e-bol.net 3 | Benutzer- und Rechtemanagement 3.3.1 Eine Session starten Bitte beachten Sie, dass Ihre PHP-Installation Sessions nicht automatisch starten sollte. Das heißt, die Direktive session.auto_start sollte in der Datei php.ini mit dem Wert 0 belegt werden. Um eine Session mit Zend_Session zu nutzen, rufen Sie die statische Methode Session_Zend: :start() auf. Der Aufruf der Methode ist zwar nicht unbedingt notwendig, da sie automatisch beim Anlegen des Namensraums ausgeführt wird, aber start!) bietet zwei Vorteile. Zum ersten ist für alle, die den Code später lesen, eindeutig, dass mit Sessions gearbeitet wird. Der zweite Vorteil ist, dass Sie den Aufruf der Methode »weit vorne« im Code, also beispielsweise im Bootstrap- File platzieren können. Da start!), ebenso wie das normale Session-Handling in PHP, ein Cookie setzen muss, würde der Methodenaufruf fehlschlagen, falls schon Daten an den Client gesendet worden wären. Nachdem Sie eine Session gestartet haben, können Sie ein neues Objekt der Klasse Zend_Session_Namespace ableiten. Der Konstruktor der Klasse startet die Session übrigens auch implizit, wenn Sie das zuvor noch nicht gemacht haben sollten Der Konstruktor erwartet einen String, den Namespace, als Parameter, um die Variablen zuordnen zu können. Nutzen Sie den Namespace dann inner- halb eines anderen Scripts, das in derselben Session ausgeführt wird, so stehen die Werte, die in dem Namespace abgelegt wurden, wieder zur Verfügung. Bitte beachten Sie, dass der Name der Namespaces nicht mit Zend_ beginnen sollte, da dieses Präfix für die Klassen des Zend Frameworks reserviert ist. Auch darf der Name nicht mit einem Unterstrich beginnen. Übergeben Sie keinen Namen, so wird ein Namenspace mit dem Namen Default genutzt. Diesen Namen sollten Sie also ebenfalls nicht für einen eigenen Namespace nutzen, um Überschneidungen zu verhindern. Nachdem Sie ein Namespace-Objekt abgeleitet haben, können Sie die Session- Daten darin ablegen, indem Sie einer beliebigen Eigenschaft einen Wert oder ein Array zuweisen. Im folgenden kleinen Beispiel wird der Zeitstempel der Log-in- Zeit in einer Session abgelegt: require_once ’Zend/Session.php'; requi re_once 'Zend/Session/Namespace.php'; // Session starten Zend_Session::start(); // Neuen Namespace registrieren Ssession = new Zend_Session_Namespace('UserDaten'); 130
e-bol.net Session-Verwaltung mithilfe von Zend_Session | 3-3 // Wurde der Wert schon in der Session abgelegt? if (false —== isset($session->login_zeit)) { // Noch nicht => Erster Aufruf => Zeit speichern $session->login_zeit = timeO; } echo "Sie haben sich um ". date( ’H:i :s’, $session->login_zeit). " Uhr eingeloggt.": Listing 3.6 Nutzung von Zend_Session Sie sehen - die Nutzung ist einfach und effizient. Zu beachten ist allerdings, dass Namespaces mit gleichem Namen dasselbe Objekt referenzieren, auch wenn das auf den ersten Blick unwahrscheinlich erscheint. Im folgenden Beispiel werden zwei eigenständige Objekte instantiiert, bei denen die Namen der Namespaces allerdings identisch sind: Ssession = new Zend_Session_Namespace('MeinSpace'): $session_2 = new Zend_Session_Namespace('MeinSpace'): $session->wert =’Hallo': $session_2->wert ='Welt': echo $session->wert: // Gibt Welt aus echo $session_2->wert: // Gibt Welt aus Listing 3.7 Umgang mit Namespaces Ob dieses Verhalten nun einen Vor- oder ein Nachteil darstellt, wird wohl jeder für sich selbst entscheiden müssen. Möchten Sie allerdings verhindern, dass ein schon genutzter Namensraum noch einmal genutzt werden kann, so können Sie dem Konstruktor der Klasse Zend_Sessi on_Namespace als zweiten Parameter den Wert true übergeben. Versucht Ihr Script dann denselben Namensraum noch einmal zu initialisieren, resultiert dies in einer Exception: Ssession = new Zend_Session_Namespace('MeinSpace',true): // Die nächste Zeile wirft eine Exception $session_2 = new Zend_Session_Namespace('MeinSpace'): Das ist natürlich nur dann der Fall, wenn der Namensraum innerhalb eines Scripts doppelt initialisiert werden soll. Handelt es sich um einen zweiten Aufruf des Scripts oder um ein anderes Script, so ist das kein Problem. 131
e-bol.net 3 | Benutzer- und Rechtemanagement 3.3.2 Gültigkeit und Schutz von Session-Daten Die Daten, die Sie in einer Session ablegen, sind standardmäßig so lange gültig, wie der Browser nicht geschlossen wird und der Server die Session-ID von ihm ausle- sen kann. Zend_Sessi on gibt Ihnen aber die Möglichkeit, die gesamte Session oder auch nur einzelne Werte in der Session mit einem Time-out zu versehen, was unge- mein praktisch ist. Nach Ablauf des Time-outs verfallen die Daten automatisch. Hierbei sind zwei Fälle zu unterscheiden: Um eine ganze Session mit einem Time-out zu versehen, rufen Sie die statische Methode rememberMe() aus der Klasse Zend_Sessi on auf und übergeben ihr die Anzahl von Sekunden, für die die Session gültig sein soll. In diesem Fall schickt PHP ein persistentes Cookie an den Client. Damit kann die Session auch dann noch aufgenommen werden, falls der Browser zwischenzeitlich geschlossen wurde. Andererseits verfällt das Cookie, wenn der Browser noch geöffnet und der Verfallszeitpunkt erreicht ist. Wichtig ist, dass Sie die Methode vor dem Start der Session bzw. vor dem Ableiten eines neuen Namespaces aufrufen: // Session bleibt eine Stunde gueltig Zend_Session::rememberMe(3600): Zend_Session::start(); Ssession = new Zend_Session_Namespace(’MeinSpace',true): Möchten Sie die Session vorher beenden, beispielsweise weil der Benutzer sich abgemeldet hat, so stehen Ihnen mehrere Möglichkeiten zur Verfügung. Die erste Möglichkeit ist, dass Sie die Methode Zend_Session:: forgetMe() vor dem nächsten Start der Session aufrufen. In diesem Fall wird die Lebensdauer des Cookies auf 0 gesetzt. Somit wird das persistente Cookie zu einem normalen Ses- sion-Cookie und verfällt, wenn der Browser geschlossen wird. Bis zu diesem Zeit- punkt bleibt die Session allerdings noch bestehen. Die zweite Möglichkeit ist die Nutzung der Methode expi reSessi onCooki e(). Auch sie muss vor dem Start der Session aufgerufen werden und legt das Verfallsdatum des Cookies in die Vergan- genheit, sodass das Cookie sofort ungültig wird. Die dritte Möglichkeit besteht darin, Zend_Session: :destroy() zu nutzen. Diese Methode können Sie allerdings erst nach dem Start der Session einsetzen. Bei dieser Methode handelt es sich im Endeffekt um session_destroy() in PHP mit ein paar zusätzlichen Features. Sie können der Methode nämlich noch zwei boo- lesche Werte mit auf den Weg geben. Der erste definiert, ob das Session-Cookie »ungültig gemacht« werden soll. Übergeben Sie hier true, was gleichzeitig auch der Default-Wert ist, so wird das Verfallsdatum des Cookies in die Vergangenheit gelegt. Der zweite Parameter, der standardmäßig ebenfalls true ist, legt fest, ob die Session-Daten mit einem Schreibschutz versehen werden sollen (= true) oder noch geschrieben werden können (= fal se). 132
e-bol.net Session-Verwaltung mithilfe von Zend_Session | 3-3 3.3.3 Nutzung eigener Session-Save-Handler Wie eingangs bereits erwähnt, bietet Zend_Session auch die Möglichkeit, einen eigenen Session-Save-Handler zu nutzen. Standardmäßig werden die Session- Daten als Datei im temporären Verzeichnis des Servers abgelegt. Das ist aller- dings nicht immer gewünscht. Abgesehen davon, dass die Festplatte bzw. das Verzeichnis zu voll werden kann, stellt diese Vorgehensweise ein Problem dar, wenn ein Benutzer von einem zu einem anderen Server weitergeleitet werden soll, da dieser die Session-Daten nicht kennt. Das Gleiche kann natürlich auch dann passieren, wenn Sie Server im Cluster betreiben. Einen anderen Save-Handler können Sie mit der statischen Methode setSave- Handl er() anmelden, welche aufgerufen werden muss, bevor die Session gestar- tet wird. Die Methode erhält ein Objekt einer Klasse übergeben, die das Interface Zend_Session_SaveHandler_Interface implementieren muss. Das Interface ist folgendermaßen deklariert: interface Zend_Session_SaveHandler_Interface ( public function open($save_path, $name); public function closeO; public function read($id); public function write($id, $data); public function destroy($id): public function gc($maxlifetime); Ich möchte hier nicht ausführlich auf die Funktionalität der einzelnen Methoden eingehen. Sollten Sie schon einmal einen eigenen Save-Handler erstellt haben, so kennen Sie die Funktionalität bereits. Andernfalls werfen Sie bitte einen kurzen Blick in PHP-Dokumentation zur Funktion session_set_save_handl er(). Sie finden sie unter der folgenden Internet-Adresse: http://www.php.net/manual/ de/function.session-set-save-handler.php. Dennoch möchte ich Ihnen eine kleine Beispiel-Implementation mithilfe von Zend_Db vorstellen. Möchten Sie einen eigenen Save-Handler implementieren, so ist bei Zend_Session eine Besonderheit zu beachten. Aus Sicherheitsgründen generiert die Klasse immer eine neue Session-ID, wenn das Objekt instanziiert wird. Das hat zur Folge, dass Ihr Save-Handler zwar beim Lesen noch die »alte« Session-ID bekommt, beim Speichern der Daten aber bereits die neue genutzt wird. Sie sollten also immer dafür sorgen, dass die Daten, die mit der alten Ses- sion-ID gespeichert wurden, entfernt werden. 133
e-bol.net 3 | Benutzer- und Rechtemanagement Das folgende Beispiel basiert auf dieser Tabelle: CREATE TABLE 'session_data' ( 'sid' char(32) NOT NULL, 'timestmp' int(ll) DEFAULT NULL, 'data' blob, PRIMARY KEY ('sid' ) ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Listing 3.8 Tabelle für Session-Daten Die Spalte sid speichert die Session-ID, die Spalte timestmp die Uhrzeit, zu der die Session gestartet wurde, und in der Spalte data werden die eigentlichen Daten abgelegt. Die Klasse für den Save-Handler könnte dann folgendermaßen implementiert werden: require_once ’Zend/Db.php’; require_once ’Zend/Session/SaveHandler/Interface.php’; dass SaveHandler implements Zend_Session_SaveHandler_Interface { private $db = nul1; private $old_sid = null; private $startime = null; // Öffnen der Verbindung zur Datenbank public function open($save_path, $name) { // Verbindungsdaten $config = array( 'host’ => '127.0.0.1’, 'username' => 'root', 'password' => '', ’dbname' => 'test' ); $this->db = Zend_Db::factory(’Mysqli',$config); // Schließen der Datenbankverbindung public function closed { $this->db->closeConnection(); 134
e-bol.net Session-Verwaltung mithilfe von Zend_Session | 3-3 // Auslesen der Session-Daten public function read($id) { // Daten auslesen $sql = "SELECT timestmp, data FROM session_data WHERE sid = ?"; $res = $this->db->fetchAl 1($sql . $id); // Haben wir Daten bekommen? if (true === is_array($res) && 1 === count($res)) // Alte ID abspeichern um alte Daten // aktualisieren zu können $this->old_sid = Sid: // Ursprüngliche Startzeit merken Sthis->startime = SresEO]['timestmp; // Session-Daten zurückgeben return $res[0][’data ' ]; 1 return nul1 ; // Speichern der Session-Daten public function write($id, $data) { // Gibt es alte Daten? if (null !== $this->startime) // Daten mit alter ID entfernen Ssql = "DELETE FROM session_data WHERE sid = ?"; $this->db->query($sql, $this->old_si d); // Startzeit der Session merken Stirne = $this->startime; 1 el se // Neue Session => aktuelle Zeit als Startzeit Stirne =time(); 1 // Daten in Tabelle abspeichern Ssql = "INSERT INTO session_data 135
e-bol.net 3 | Benutzer- und Rechtemanagement (sid, timestmp, data) VALUES (?. ?, ?)"; $this->db->query($sql . array($id,$time, $data)); return true; // Session-Daten löschen public function destroy($id) { $sql = "DELETE FROM session_data WHERE sid = ?"; $this->db->query(Ssql, Sid): // Müllabfuhr => alle veralteten Daten löschen public function gc(Smaxlifetime) { Ssql = "DELETE FROM session_data WHERE timestmp < ?"; Stirne = time()-Smaxlifetime; Sthis->db->query(Ssql, Stirne); } Listing 3.9 Implementation eines eigenen Session-Händlers Wie Sie sehen, ist es etwas aufwändiger, einen Session-Händler für diese Klasse zu implementieren. Aber es ist durchaus handhabbar. Das hier vorgestellte Beispiel sollten Sie allerdings nicht als Referenz-Implemen- tation ansehen. Es sollte lediglich die Vorgehensweise beispielhaft erläutern und ist nicht für den Produktiveinsatz gedacht. 136
e-bol.net People get annoyed whenyou try to debug them. - Larry Wall (Open Sources, 2999 0'Reilly and Associates) 4 Infrastruktur-Klassen Dieses Kapitel behandelt verschiedene Klassen, die bei der Entwicklung einer Applikation im Hintergrund arbeiten. Neben der Möglichkeit, die Performance mit Caching zu steigern, finden Sie hier auch Features, um Logs zu schreiben oder Eingaben zu filtern. 4.1 Performance-Optimierung mit Zend_Cache Die Nutzung ausgefeilter Caching-Strategien kann einen Webserver bzw. den Datenbankserver deutlich entlasten. Zend_Cache verfolgt hierbei den typischen Ansatz eines User-Land Caches. Das heißt, es werden fertig aufbereitete Seiten oder die Ergebnisse von Funktionen gecachet. Es handelt sich hierbei nicht um einen Opcode-Cache, welcher den fertig interpretierten PHP-Code cachet und somit die Ausführung des Codes beschleunigt. Allerdings können Sie Zend_Cache in Kombination mit dem Opcode-Cache APC (Alternative PHP Cache) nutzen. Sie finden beide im PECL-Repositoiy (http://pecl.php.net). Gerade bei größeren Installationen ist aber auch die Nutzung von Zend Platform eine sehr spannende Alternative. Die Funktionsweise von Zend_Cache ist recht einfach. Zuerst müssen Sie sich ent- scheiden, was Sie cachen wollen. Damit ist auch festgelegt, welches Frontend Sie nutzen. Es stellt sich dann die Frage, wo die Daten abgelegt werden sollen. Sollen die zu speichernden Daten auf der Festplatte oder an einem anderen Ort gespei- chert werden? Diese Angabe legt fest, welches Backend Sie nutzen. Damit haben Sie auch schon alle Informationen, um loszulegen. Diese Daten werden an eine statische Methode namens factoryt) übergeben, die Ihnen das benötigte Objekt liefert. Das so generierte Objekt kann dann Daten im Cache ablegen bzw. Daten, die bereits im Cache liegen, wieder auslesen und zur Verfügung stellen. Um Daten aus dem Cache auszulesen, müssen diese natürlich eindeutig referenzier- bar sein. Das heißt, für jeden Eintrag im Cache benötigen Sie einen eindeutigen Bezeichner, beispielsweise in Form einer ID. 137
e-bol.net 4 | Infrastruktur-Klassen Das folgende kleine Beispiel zeigt, wie die komplette Ausgabe eines Scripts ge- cachet werden kann. require_once ' Zend/Cache.php'; // Die Ausgabe soll gecachet werden $frontend = 'Output'; // Speichern in Datei Sbackend = 'Fi 1e’; // Generiert das benötigte Objekt Scache = Zend_Cache::factory($frontend, $backend): // Generieren einer eindeutigen ID Sid = md5($_SERVER['PHP_SELF’]); // Daten auslesen oder Caching starten $is_cached = $cache->start(Sid); // Waren die Daten im Cache? if (false — Sis_cached) { echo 'Aktuelle Zeit: '. date(*H:i:s'); // Caching beenden $cache->end(); 1 Listing 4.1 Caching von Ausgaben Die statische Methode factory bekommt als ersten Parameter die Information übergeben, was gecachet werden soll. In diesem Fall wird das Frontend Output genutzt, welches die Ausgabe des gesamten Scripts mithilfe von Output Buffering cachet. Der zweite Parameter File, sorgt dafür, dass die Daten in einer Datei gespeichert werden. Optional können Sie noch zwei weitere Parameter, Arrays mit Optionen, nutzen. Auf diese werde ich gleich noch eingehen. Das wirklich Interessante in diesem Beispiel ist die Methode s t a r t (), welche die ID übergeben bekommt. Sie erledigt mehrere Dinge auf einmal. Sie prüft zunächst, ob die Daten bereits im Cache liegen. Ist das der Fall, so werden sie direkt ausgegeben. Die Methode gibt in diesem Fall true zurück. Falls die Daten noch nicht gespeichert waren, liefert sie den Wert false. Dieser Rückgabewert wird in der nachfolgenden i f-Abfrage geprüft. Sind die Daten noch nicht ausge- geben worden, so erfolgt das hier. Die Methode end(), die anschließend aufge- rufen wird, sorgt dafür, dass die Daten ausgegeben (zuvor waren sie ja nur im 138
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 Ausgabepuffer) und korrekt im Backend (in diesem Fall also in einer Datei) gespeichert werden. Grundsätzlich finden Sie diese Vorgehensweise bei jeder Kombination aus Front- end und Backend wieder. Allerdings variieren die Namen der Methoden etwas. 4.1.1 Frontends Die schon erwähnten Optionen, mit denen Sie das Verhalten des Frontends steu- ern können, gliedern sich in allgemeine und spezielle Optionen, die zusammen in einem Array übergeben werden. Nachfolgend finden Sie eine Aufstellung der allgemeinen Optionen. Die speziellen Optionen werden später bei den jeweili- gen Frontends erläutert. Derjenige Schlüssel, den Sie am häufigsten benötigen werden, ist lifetime. Er steuert, wie lange die Daten im Cache gültig sind. Die Gültigkeitsdauer wird als Zahl in Sekunden angegeben. Mit dem Standardwert 3600 bleiben die Daten also eine Stunde im Cache (60 Sekunden * 60 Minuten - 3600). Übergeben Sie hier null, so bleiben die Daten unbegrenzt gültig. Sehr hilfreich kann automatic_serialization sein. Übergeben Sie mit diesem Schlüssel den Wert true (der Default-Wert ist fal se), so werden die zu cachen- den Daten zuerst serialisiert, bevor sie im Cache abgelegt werden. Das ist immer dann sehr hilfreich, wenn Sie Datenstrukturen wie Arrays oder Objekte spei- chern wollen, da diese nach dem Auslesen sonst nicht mehr genutzt werden kön- nen. Bei skalaren Typen wie Integer oder String ist es nicht notwendig, diese Funktion einzuschalten, da sie das Lesen und Speichern der Daten verlangsamt. Allerdings werden dann alle Werte in Form eins Strings zurückgegeben, was in PHP üblicherweise nicht weiter tragisch ist. Übrigens handelt es sich bei dem Frontend Output um einen String, der nicht serialisiert werden muss. Praktisch ist auch caching. Mit diesem Schlüssel können Sie einen booleschen Wert übergeben. Der Default-Wert true sorgt dafür, dass das Caching eingeschal- tet ist. Mit fal se deaktivieren Sie den Speichermechanismus. Diese Funktionali- tät kann sehr hilfreich sein, wenn Sie Ihre Software testen. Andernfalls müssten Sie immer warten, bis der Cache verfallen ist, um die aktuelle Ausgabe zu sehen. Der Schalter wri te_control dient dazu, die Daten, nachdem sie im Cache abge- legt wurden, zu überprüfen. Dazu werden sie nach dem Speichern einmal gele- sen, was zwar etwas Performance kostet, die Datensicherheit dafür aber erhöht. Mit dem Wert true (der Default-Wert) schalten Sie diese Funktion ein. 139
e-bol.net 4 | Infrastruktur-Klassen Des Weiteren können Sie mit dem Schlüssel 1 oggi ng, der auch boolesche Werte akzeptiert, das Logging mittels Zend_Log aktivieren, um einen Überblick über das Verhalten von Zend_Cache zu erhalten. Gerade bei großen Installationen kann es hilfreich sein, Einfluss auf die Garbage Collection zu nehmen. Mit Garbage Collection wird die Funktionalität bezeich- net, die dafür zuständig ist, veraltete Cache-Einträge zu löschen. Diese Müllab- fuhr wird immer dann im Hintergrund ausgeführt, wenn Sie etwas im Cache ablegen. Die Notwendigkeit für eine solche Funktion ergibt sich daraus, dass unter Umständen sehr viele veraltete Informationen im Cache liegen, die nicht mehr ausgeliefert werden. Das kann zum Beispiel dann passieren, wenn Sie eine Community betreiben, in der jeder Benutzer eine personalisierte Startseite hat. Die gecacheten Daten können schon lange veraltet sein, weil der Benutzer sich seit Wochen oder Monaten nicht mehr angemeldet hat. Um zu verhindern, dass die gecacheten Daten überhandnehmen, können Sie den Schlüssel automatic_ cleaning_factor nutzen. Mit seiner Hilfe wird ein Integer-Wert übergeben. Handelt es sich dabei um 0, so werden die Daten nicht gelöscht. Bei einer Zahl x größer als Null wird in einer von x Cache-Schreiboperationen ein Löschvorgang initiiert. Das heißt, bei einer 1 werden bei jedem Schreibvorgang die veralteten Daten gelöscht. Bei einer 10 wird in einer von zehn Operationen die Garbage Collection gestartet usw. Welcher Wert für Ihre Zwecke ideal ist, muss individu- ell entschieden werden. Ein sehr häufiges Löschen verlangsamt viele Schreibope- rationen. Seltenes Löschen macht sich nur selten bemerkbar, dauert dafür aber länger. Möchten Sie die automatische Garbage Collection aus Performance-Grün- den nicht nutzen, so können Sie die Daten auch manuell löschen. Weitere Infor- mationen dazu erhalten Sie in Abschnitt 4.1.3. Das Frontend »Output« Zum Großteil haben Sie das Frontend ja schon in den vorangegangenen Absätzen kennengelernt. Dennoch möchte ich noch einige Ergänzungen anbringen. In Lis- ting 4.1 wird die Ausgabe eines ganzen Scripts gecachet. Das Frontend Output ist allerdings auch in der Lage, nur die Ausgabe bestimmter Bereiche zu speichern. Wenn Sie also nur einen Teil einer Seite speichern wollen, so rufen Sie start() erst an der Stelle auf, ab der gecachet werden soll. Nach dem Aufruf der Methode end() können Sie wieder direkt Daten ausgeben. Somit verfügen Sie über ein sehr hohes Maß an Flexibilität. Möchten Sie nur einen Teil der Seite cachen, dann sollten Sie die ID natürlich nicht nur auf Basis von $_SERVER[' PHP_SELF' ] erstellen. In so einem Fall könnten Sie die Teile beispielsweise zusätzlich durch- nummerieren. 140
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 Wie bereits erwähnt, können Sie der Methode factory ein Array mit Optionen übergeben. Output kennt keine eigenen Optionen, sodass Sie nur auf die allge- meinen zugreifen können. Die Methode start() akzeptiert neben der ID noch einen zweiten Parameter. Übergeben Sie an dieser Stelle true, wird nicht geprüft, ob die gecachten Daten noch gültig sind. Die Gültigkeitsdauer wird ignoriert und der Cache-Inhalt bleibt beliebig lange gültig. Auch die Methode end() akzeptiert noch Parameter. An erster Stelle können Sie hier ein Array mit Tags übergeben, die dann dem Cache-Eintrag zugeordnet wer- den. Als zweiten Parameter können Sie noch eine spezielle Lebensdauer für den Cache-Eintrag übergeben. Somit ist es möglich, den Default-Wert oder den Wert, den Sie via Array übergeben haben, zu überschreiben. Cachen von Seiten mit dem Frontend »Page« Das Frontend Page ist der Spezialist, wenn es darum geht, die Ausgabe ganzer Seiten zu cachen. Die Flexibilität von Output, nur einzelne Blöcke in den Zwi- schenspeicher auszulagern, ist hier nicht gegeben. Bei einer einfachen Seite, deren Ausgabe nicht von eingehenden GET-, POST-, Session- oder sonstigen Werten abhängt, können Sie beruhigt auf Output setzen. Die meisten PHP-Seiten werden allerdings auf Inhalte bestimmter Variablen reagieren. In diesem Fall bie- tet sich die Nutzung von Page an. Page generiert die benötigte Cache-ID nämlich automatisch, und Sie können dem Frontend dabei mitteilen, ob bestimmte Vari- ablen beim Generieren der Cache-ID einbezogen werden sollen. Auf diesem Weg kann so sichergestellt werden, dass eine Seite nur dann aus dem Cache kommt, wenn beispielsweise auch die übergebenen GET-Werte identisch sind. Dieses Verhalten wird über die speziellen Optionen gesteuert, die in diesem Fall zur Verfügung stehen. In dem Optionen-Array können Sie mit dem Schlüssel defaul t_options ein verschachteltes Array übergeben, in dem Sie die Schlüssel aus Tabelle 4.1 mit einem booleschen Wert belegen. Schlüssel Default-Wert cache_wi th_get_vari abl es fal se cache_wi th_post_vari a b1 es fal se cache_wi th.sessi on_variables fal se cache_wi th_f 11es_vari a b1 es fal se cache_wi th_cooki e_vari ables fal se make_i d_with_get_variables true make_i d_with_post_vari ables true Tabelle 4.1 Mögliche Schlüssel für Optionen 141
e-bol.net 4 | Infrastruktur-Klassen Schlüssel Default-Wert make_i d_wi th_sessi on_vari abl es true make_i d_with_files_vari ables true make_i d_with_cookie_vari ables true Tabelle 4.1 Mögliche Schlüssel für Optionen (Forts.) Die Schlüssel, die mit make_id beginnen, sorgen jeweils dafür, dass die danach genannten, superglobalen Arrays mit in die ID einbezogen werden. Standardmä- ßig gilt das für jedes der superglobalen Arrays, was meist auch sinnvoll ist. Die cache_wi th-Schlüssel geben Ihnen die Möglichkeit, bestimmte superglobale Arrays mit im Cache abzulegen. Allerdings ist dies nur bedingt hilfreich, da die Seite eh aus dem Cache kommt. In diesem Array können Sie auch noch den Schlüssel cache nutzen, dem Sie einen booleschen Wert übergeben können. Dieser überschreibt den Wert des Schlüssels caching aus den allgemeinen Optionen. Der Hintergrund dieser Mög- lichkeit wird gleich plausibel. Ein ganz besonders interessantes Feature besteht darin, dass Sie das Caching anhand der URL steuern können. Die Idee dahinter ist: Vielleicht haben Sie das ganze Projekt auf Basis des MVC-Patterns aufgebaut und die Caching-Funktiona- lität ist nur einmal im Front Controller vorhanden. Allerdings greift der Mecha- nismus auch ohne MVC, da lediglich nur die aufgerufene URL auf Basis eines regulären Ausdrucks geprüft wird. Der reguläre Ausdruck wird dabei als Schlüs- sel in einem Array genutzt, der wiederum auf ein Array verweist. In diesem Array können Sie dann erneut alle Optionen nutzen, die bereits oben unter dem Schlüssel defaul t_options erläutert wurden. Stimmt der reguläre Ausdruck mit der URL überein, wird die aktuelle Ausgabe entsprechend den Vorgaben ge- cachet oder eben auch nicht. Somit wird klar, warum Sie mit cache steuern kön- nen, ob die Seite gecachet wird. Sie können dies somit anhand der URL entschei- den. Neben den vorgenannten Schlüsseln können Sie auch noch debug_header auf true oder fal se setzen. Übergeben Sie true, wird in den Kopf einer Seite die aus dem Cache kommt, DEBUG HEADER : Thi s i s a cached page ! eingefügt. Das kann bei der Fehlersuche durchaus sehr praktisch sein. In Listing 4.2 sehen Sie die Funktionsweise. Hierbei handelt es sich um eine Datei, die an erster Stelle in anderen Dateien inkludiert werden kann: require_once ’Zend/Cache.php'; // Die komplette Seite soll gecachet werden 142
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 $frontend = ’Page' ; $opts = array( // Einen Tag im Cache 'lifetime' => 24*60*60, // Globales Verhalten 'default_options' => array( 'cache_with_post_variables' => true, 1make_id_with_session_variables1 => false, 1make_id_with_cookie_variables' => false, ’cache’=>true // Verhalten fuer einzelne Dateien ’regexps’ => array ( // Einstellungen fuer index.php 'A/index.php' => array ( 'cache'=>false ), // Konfiguration fuer Unterverzeichnis /mail 'A/mail '=> array ( 'make_id_with_session_variables' => true, 'make_id_with_cookie_variables' => true, 'cache' => true // Speichern in Datei Sbackend = 'Fi 1e’; // Generiert das benötigte Objekt Scache = Zend_Cache::factory($frontend, $backend, $opts); // Daten auslesen oder Caching starten $cache->start(); Listing 4.2 Nutzung des Frontends Page mit regulären Ausdrücken Eine Datei, die gecachet werden soll, könnte so aussehen: require_once 'cache.php’; echo date( ’H:i:s’): 143
e-bol.net 4 | Infrastruktur-Klassen Wird diese Datei unter dem Namen index.php im Root-Verzeichnis gespeichert, wird sie nach den Regeln, wie sie mithilfe der regulären Ausdrücke definiert wur- den, nicht zwischengespeichert. Würden Sie die Datei allerdings im Unterver- zeichnis /mail speichern, so würde sich das Verhalten ändern und sie würde gecachet. In allen anderen Fällen würde das Regelwerk greifen, das mit defaul t_ options definiert ist. Die hier genutzten regulären Ausdrücke sind eher allgemeiner Natur und eignen sich für jede Art von Applikation. Basiert Ihre Applikation auf dem MVC-Pattern, so beachten Sie bitte, dass die Aufrufe von index.php/mail und /mail identisch sind. Wichtig ist noch der Hinweis, dass die Methode start() das Script auch dann automatisch beendet, wenn die Daten im Cache gefunden wurden. Wenn Sie nach dem starte) noch Operationen wie Logfile-Zugriffe oder Datenbankzu- griffe ausführen wollen, ist dies somit nicht möglich, sofern die Daten aus dem Cache kommen. Funktionsergebnisse mit dem Frontend »Function« cachen Die beiden bereits vorgestellten Frontends haben sich primär um die »Bild- schirm-Ausgabe« einer Webseite oder eines Scripts gekümmert. Die nachfolgen- den Frontends verfolgen eine andere Zielsetzung. Der erste Vertreter in dieser Reihe ist Function, mit dem Sie die Rückgabewerte von Funktionen cachen kön- nen. Damit die Ergebnisse von Funktionsaufrufen ohne Probleme gespeichert wer- den, sollten Sie beachten, dass alle Werte, die die Funktion benötigt, beim Aufruf übergeben werden müssen. Falls also die Funktion auf globale Werte oder Werte aus den superglobalen Arrays zugreift, kann das Frontend dies nicht erkennen. Gleiches gilt, wenn die Funktion beispielsweise mit Zufallszahlen arbeitet. Im Gegensatz zu anderen User-Land Caches ist das Paket aber in der Lage, auch die direkten »Bildschirm-Ausgaben« von Funktionen zu speichern. Datei- oder Datenbankoperationen sind davon nicht betroffen. Das Frontend kennt neben den allgemeinen Optionen auch einige spezielle Mög- lichkeiten, um das Verhalten zu steuern. An erster Stelle ist hier der Schlüssel cacheByDefaul t zu nennen. Mit ihm können Sie steuern, ob die Funktionen standardmäßig gecachet werden sollen. Ist in der globalen Option caching ein true enthalten, so wird dieses von einem false, das in cacheByDefault enthal- ten ist, überschrieben. Damit haben Sie die Möglichkeit, das Caching global ein- zuschalten, die Funktionen aber getrennt zu verwalten. Enthält caching aller- dings false, so wird cacheByDefault nicht weiter ausgewertet. 144
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 Außerdem können Sie mithilfe der Schlüssel cachedFunctions und nonCached- Functions jeweils noch Arrays mit Funktionsnamen übergeben. Im ersten Fall handelt es sich um Funktionen, die immer gecachet werden (auch wenn cache- ByDefault ausgeschaltet ist), wohingegen diejenigen, die in der zweiten Liste enthalten sind, nie gespeichert werden. require_once ' Zend/Cache.php'; // Liefert die aktuelle Zeit zurück function get_time (Sformat) { $zeit = date ($format); return $zeit; l // Liefert das aktuelle Datum zurück function get_date () { $datum = date (’d.m.Y'); return Sdatum; l // Liefert eine Zufallszahl zurück function get_random() { mt_srand(100*microtime(true)): $zahl = mt_rand(); return $zahl; l // Optionen $opts = array( ’caching’=>true, 'cacheByDefault'=>true, 'cachedFunctions' => array (’get_time’), // Funktion wird immer gecachet ’nonCachedFunctions' => array ('get_random') // Funktion wird nicht gecachet ); Scache = Zend_Cache::factoryl'Function', 'File', $opts); // Dieser Aufruf wird entsprechend der Einstellungen // von cacheByDefault und caching gecachet Sdatum = $cache->call('get_date'): 145
e-bol.net 4 | Infrastruktur-Klassen echo "Aktuelles Datum: Sdatum"; echo "<br>": Sparams = array( ’ H: i: s '): $zeit = $cache->cal 1(’get_time', $params): echo "Zeit: $zeit": echo "<br>": Szufall = $cache->cal1(’get_random'); echo "Zufallszahl: Szufall"; Listing 4.3 Nutzung des Frontends »Function« Wie Sie in Listing 4.3 sehen, erfolgt der eigentliche Aufruf der Funktionen über die Methode c a 11 (). Sie bekommt als ersten Parameter den Namen der Methode übergeben, die aufgerufen werden soll. Benötigt die Funktion Parameter, so übergeben Sie diese als zweiten Parameter in Form eines Arrays. Speichern von Methodenaufrufen mit dem Frontend »Class« Möchten Sie die Ergebnisse von Methodenaufrufen Zwischenspeichern, so gibt es dafür das Frontend Class. Dieses funktioniert im Prinzip sehr ähnlich wie das Frontend Function. Und es bietet auch die gleichen Optionen. Darüber hinaus erwartet dieses Frontend aber noch eine zusätzliche Option, die mit dem Array übergeben werden muss. Hierbei handelt es sich um den Schlüssel cachedEn- ti ty, der entweder den Namen einer Klasse oder bereits ein Objekt der entspre- chenden Klasse übergeben bekommt. Weisen Sie der Eigenschaft ein Objekt zu, so erfolgt der Aufruf der Methode dynamisch, wohingegen ein statischer Aufruf durchgeführt wird, wenn Sie den Namen zuweisen. Um die Methoden aufzurufen, hängen Sie den Namen der gewünschten Funk- tion mithilfe des ->-Operators direkt an das Cache-Objekt an. In beiden Fällen tun Sie also einfach so, als würde es sich um eine Methode des Cache-Objekts handeln, die dynamisch aufgerufen wird. Benötigt die Methode Parameter, so werden diese beim Aufruf direkt mit übergeben. require_once 'Zend/Cache.php’; class datum { private $timestamp: public function ____construct($timestamp) $this->timestamp = Stimestamp: 146
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 ) public function getDayName () return date('l',$this->timestamp); l public static function getTimestamp() return time(); l } // Beispiel für einen dynamischen Aufruf // Optionen $opts = array( 'caching' => true, ’cacheByDefault' => true, ’cachedEntity' => new datum(time()) ); Scache = Zend_Cache::factory('Glass', 'File', $opts): $name = $cache->getDayName(); echo $name; echo "<br>"; // Beispiel für einen statischen Aufruf // Optionen $opts = array( 'caching' => true, ’cacheByDefault' => true, ’cachedEntity' => "datum" ); Scache = Zend_Cache::factory('Glass', 'File', $opts): Stimestamp = $cache->getTimestamp(); echo $timestamp; Listing 4.4 Cachen von AAethodenaufrufen Dateien mit dem Frontend »File« cachen Die Idee hinter dem Frontend File ist nicht, die Ausgabe von Daten in Dateien zu cachen, wie man vielleicht zuerst glauben mag. Es geht vielmehr darum, das Ein- lesen von Dateien zu beschleunigen. Dabei ist es natürlich nicht sinnvoll, eine Datei zu cachen, die einfach nur eingelesen und anschließend direkt genutzt wer- 147
e-bol.net 4 | Infrastruktur-Klassen den soll. Insbesondere dann, wenn Sie den Cache als Datei anlegen, hätten Sie damit nichts gewonnen. Die Idee ist vielmehr, dass das Ergebnis eines Lesevor- gangs im Cache abgelegt wird. Wenn Sie also beispielsweise eine Konfigurations- datei einlesen, diese analysieren und das Ergebnis in Form eines Arrays oder eines Objekts bereitstellen, kann dieses Array oder Objekt im Cache abgelegt werden. Sobald die ursprüngliche Datei verändert wird, erkennt das Frontend dies und verwirft den Inhalt des Caches. Eine sehr praktische Lösung, wie ich finde. Die Nutzung des Frontends gestaltet sich recht einfach und erinnert ein wenig an das Frontend Output. Wichtig ist allerdings, dass File mindestens eine Option benötigt, nämlich master_file. Mit ihr definieren Sie, welche Datei eingelesen und überwacht werden muss. Sollten Sie ein Array oder ein Objekt im Cache ablegen wollen, müssen Sie darüber hinaus noch die automatische Serialisierung mithilfe von automatic_seri al ization aktivieren. Für die Nutzung von File müssen Sie wiederum manuell eine ID generieren, wobei es sich anbietet, den Namen der zu überwachenden Datei inklusive des absoluten Pfads zu nutzen. Die ID übergeben Sie an die Methode load(), welche versucht, die Daten aus dem Cache zu lesen. Ist das nicht möglich, liefert sie fal se zurück. Um die Daten zu speichern, greifen Sie auf save() zurück. Hiermit werden die Daten im selek- tierten Backend abgelegt. Das Cache-Objekt hat sich die ID, mit der gearbeitet wird, übrigens schon beim Ladeversuch »gemerkt«, sodass die Methode außer den Daten keine weiteren Informationen mehr benötigt. Optional können Sie die ID noch als zweiten Parameter angeben, sofern dies erforderlich sein sollte. require_once ' Zend/Cache.php'; // Frontend File $frontend = 'File'; Sdatei = '/var/www/wwwl/config.ini' ; Sid = md5(Sdatei); Sopts = array ( 'master_fi1e’ => Sdatei, 'automatic_serialization’ => true); // Speichern in Datei Sbackend = 'Fi 1e’; // Generiert das benötigte Objekt Scache = Zend_Cache::factory($frontend, Sbackend, Sopts); 148
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 // Daten auslesen oder Caching starten Sdaten = $cache->load($id); if (false — $daten) { $daten = parse_ini_file($datei); $cache->save(tdaten); } // Weiterer Programmablauf Listing 4.5 Nutzung des Frontends »File« Das Frontend »Core« Das letzte Frontend, das ich Ihnen vorstellen möchte, ist Core. Bei genauer Betrachtung ist Core vielleicht kein echtes Frontend, sondern eher »die Mutter aller Frontends«. Das liegt daran, dass Core diejenige Klasse ist, auf der alle ande- ren Frontends aufbauen. Mithilfe von Core können Sie somit recht einfach ein eigenes Frontend erstellen bzw. beliebige Inhalte cachen, für die noch kein Front- end vorgesehen ist. Die Funktionsweise von Core ist weitestgehend identisch mit der von File. Daher möchte ich nicht weiter auf Core eingehen. Sollten Sie allerdings einmal ein eigenes Frontend entwickeln wollen, dann ist diese Klasse Ihr Kandidat. Eine Erläuterung des Frontends finden Sie im Manual zum Zend Framework. 4.1.2 Nutzung von Backends Nachdem Sie nun wissen, was Sie wie speichern können, stellt sich noch die Frage, wo die Daten abgelegt werden können. Hierfür stehen verschiedene Backends zur Verfügung. In den Beispielen wurde immer das Backend File genutzt, das die Daten auf der Festplatte ablegt. File stellt in den meisten Fällen tatsächlich eine wirklich gute Lösung dar, da der Zugriff auf Dateien sehr schnell ist und kein großer Rechenaufwand erforderlich ist. Die Backends lassen sich teilweise noch weiter konfigurieren. Dazu übergeben Sie der Methode factory() noch ein weiteres Array mit Optionen als vierten Parameter. Caching auf der Festplatte mit dem Backend »File« Die grundsätzliche Funktionsweise von File kennen Sie nun bereits. Standardmä- ßig werden die Dateien, die die Daten enthalten, im temporären Ordner des Ser- vers abgelegt, also meist in /tmp. Das kann, sofern mehrere Domains auf dem Server laufen, unter Umständen ein Problem sein, da es passieren kann, dass die 149
e-bol.net 4 | Infrastruktur-Klassen Dateien nicht mehr eindeutig sind. Des Weiteren sind andere Benutzer eventuell in der Lage, den Inhalt der Cache-Dateien auslesen, was ein Sicherheitsproblem darstellen würde. Möchten Sie die Daten in einem anderen Verzeichnis ablegen, so können Sie in dem Array, das als vierter Parameter genutzt werden kann, mithilfe des Schlüssel cache_di r ein anderes Verzeichnis festlegen. Der Schlüssel fi 1 e_l ockl ng, der boolesche Werte akzeptiert, gibt Ihnen die Möglichkeit festzulegen, ob die Cache-Dateien während der Zugriffe mit einem Lock gesperrt werden sollen. Dies verhindert, dass eine unvollständige Datei aus- geliefert wird. Anzumerken ist allerdings, dass diese Vorgehensweise nicht auf allen Plattformen genutzt werden und den Zugriff verlangsamen kann. Eine Alternative, um zu verhindern, dass verfälschte Daten ausgeliefert werden, ist die Nutzung der Lesekontrolle. Diese schalten Sie ein, indem Sie dem Schlüs- sel read_control den Wert true übergeben. Schalten Sie diese Funktion ein, wird in der Datei eine Prüfsumme mit abgelegt, die nach dem Lesen der Datei mit einer neu berechneten Summe verglichen wird. Das ist zwar ein wenig zeitauf- wändiger, kann sich bei wichtigen Daten aber durchaus lohnen. In diesem Zusammenhang kann es auch praktisch sein zu definieren, wie die Prüfsumme erstellt wird. Hierfür ist der Schlüssel read_control_type vorgese- hen. Ihm können Sie den String crc32, md5 oder strlen zuweisen. Standardmä- ßig wird die Prüfsumme mit dem CRC32-Verfahren bestimmt, welches relativ zuverlässig und recht schnell ist. MD5-Prüfsummen sind noch ein ganzes Stück präziser, allerdings auch langsamer. Mit strlen wird nur die Länge des Strings geprüft, was natürlich nicht hundertprozentig zuverlässig ist. Ich meine, dass für einen Großteil der Anwendungsfälle diese Präzision aber ausreicht und das Ver- fahren hierfür eine hohe Geschwindigkeit bietet. Sollten auf einem Server mehrere Anwendungen mit ZencLCache arbeiten, so kann es passieren, dass die Dateien nicht eindeutig zugeordnet werden, falls sie alle in einem Verzeichnis liegen. Zwar wäre es die bessere Lösung, für jede Instal- lation ein eigenes Verzeichnis zu nutzen, aber alternativ können Sie auch einfach das Präfix der Dateinamen verändern. Übergeben Sie dazu den Dateinamen, der genutzt werden soll, an den Array-Schlüssel file_name_prefix. Somit kann Zend_Cache die Dateien den einzelnen Installationen zuordnen. Caching im Speicher mit den Backends »Memcached« und »APC« Möchten Sie kleinere Datenmengen cachen, die sehr schnell zur Verfügung ste- hen sollen, so können Sie diese auch im Arbeitsspeicher des Servers ablegen. Da der Arbeitsspeicher meist deutlich kleiner ist als das Platzangebot der Festplatten, 150
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 sollten Sie überlegen, bei welchen Dateien es sinnvoll ist, diese im Speicher abzu- legen. Kleinere Datenmengen, die auf jeder Seite inkludiert werden, bieten sich hier an. Das Backend Memcached unterstützt allerdings auch die Möglichkeit, einen ande- ren Server zu nutzen, sodass der eigentliche Webserver entlastet wird. Sollten Sie diese Lösung interessant finden, würde ich vorher einen Benchmark empfehlen, um die resultierende Performance besser einzuschätzen. Ein interessanter Aspekt bei Memcached ist aber, dass Sie auch über mehrere Webserver hinweg cachen können. Wenn Sie mehrere Webserver in einem Cluster betreiben, dann muss also nur einer der Server den Cache-Eintrag generieren und alle anderen können darauf zugreifen. Zend_Cache kennt zwei Arten, um Daten im Speicher zu halten. Sie können hier- bei entweder auf APC oder Memcached zurückgreifen. Bei beiden handelt es sich um Erweiterungen aus dem PECL-Repositoiy. Unter Linux und anderen UNIX-verwandten Betriebssystemen können Sie diese Pakete mit peel install memcache bzw. peel i nstal l apc installieren. Bitte beachten Sie, dass das PECL- Paket memcache und nicht memcached heißt. Bei der Nutzung von memcached benötigen Sie zusätzlich den entsprechenden Server (http://www.danga.com/ memcached/). Installieren Sie diesen bitte über das Installationsprogramm Ihres Betriebssystems. Falls Sie Windows nutzen, so finden Sie im Internet auch fertige Varianten. Der PECL-Installer lädt die Dateien direkt aus dem Internet herunter und kompi- liert sie. Danach teilt er Ihnen mit, welche Zeile Sie in der Datei php.ini ergänzen müssen, um die Erweiterung zu laden. Nutzen Sie Windows, so finden Sie fertig kompilierte DLLs unter http://pecl4win.php.net. Dort finden Sie auch Informati- onen zur Installation der DLLs. Ich persönlich empfehle die Nutzung von APC. Auch wenn memcached ein spezi- eller Caching-Mechanismus und somit wahrscheinlich ein wenig performanter als APC ist, so bietet APC doch Vorteile. Der »Alternative PHP Cache«, kurz APC, ist ein Cache für den Byte-Code, der aus einem PHP-Script generiert wird. Das heißt, Ihr Script wird einmal übersetzt und danach im Cache gehalten. Das stei- gert die Performance Ihrer Anwendung unter Umständen noch einmal deutlich. Weitere Informationen zur Nutzung und Konfiguration von APC finden Sie unter http://www.php.net/apc. Nutzen Sie APC, können Sie den Namen des Backends einfach direkt an die Fac- tory-Methode übergeben, da APC keine weitere Parameter benötigt: Scache = Zend_Cache::factory('Class', 'APC', $opts); 151
e-bol.net 4 | Infrastruktur-Klassen Wie schon erwähnt, können Sie bei der Nutzung von memcached auch mit ande- ren Servern arbeiten. Dabei muss das Cache-Backend wissen, wie diese angespro- chen werden. Diese Information können Sie mit dem vierten Parameter als Array übergeben. Mit dem Schlüssel Servers übergeben Sie ein Array, in dem jeder Server einen eigenen Eintrag hat. Mit dem Schlüssel host geben Sie den Namen des Servers an, port enthält den genutzten Port, und mit persi stent übergeben Sie einen booleschen Wert, der definiert, ob eine persistente Verbindung genutzt werden soll. Dabei wird pro Server-Prozess eine Verbindung offen gehalten. Neben dem Array mit den Schlüsseln können Sie mit dem Schlüssel compressi on noch einen booleschen Wert übergeben, der festlegt, ob die Daten bei der Über- tragung komprimiert werden sollen. Ob diese Option sinnvoll ist, hängt von der Struktur Ihrer Daten ab. Also davon, ob sich diese komprimieren lassen. Im Zweifelsfall sollten Sie mit einem Benchmark prüfen, ob die Option Ihre Anwen- dung schneller oder eventuell sogar langsamer macht. Geben Sie diesen vierten Parameter nicht an, versucht das System eine Verbin- dung zu einem lokal installierten Memcached-Server aufzubauen. Hierbei werden der Port 11211 und eine persistente Verbindung genutzt, wobei die Kompression ausgeschaltet ist: $opts_backend = array( 'Servers' => array(array( 'host' => '192.168.0.3* . 'port' => 11211, 'persistent' => true )), 'compression’ => false ); Scache = Zend_Cache::factory('Class', 'Memcached', $opts, $opts_backend); Listing 4.6 Nutzung von »Memcached« Weitere Backends Zusätzlich zu den schon erwähnten Backends können Sie auch noch Zend Plat- form sowie SQLite als Backend einsetzen. Bei Zend Platform handelt es sich um ein kommerzielles Produkt aus dem Hause Zend. Zend Platform stellt einige wirklich sehr nützliche Funktionalitäten zur Verfügung. Sollten Sie Wert auf gute Performance, hohe Verfügbarkeit oder ein gute Überwachung des Server legen, dann könnte Zend Platform eine interessante Alternative sein. SQLite ist eine dateibasierte Datenbank, die in PHP 5 enthalten ist. Nach meinen Benchmarks ist ein direkter Dateizugriff über das Backend File allerdings performanter. 152
e-bol.net Performance-Optimierung mit Zend_Cache | 4-1 Daher gehe ich hier nicht auf diese beiden Varianten ein. Weitergehende Infor- mationen hierzu können Sie dem Manual entnehmen. 4.1.3 Manuelle Verwaltung von Cache-Einträgen In der Regel müssen Sie sich nicht darum kümmern, welche Einträge im Cache vorhanden sind und wie alt diese sind, da die Garbage Collection sich dieses Pro- blems annimmt. Nun könnte es aber sein, dass Sie gecachete Daten manuell löschen wollen. Das könnte beispielsweise dann der Fall sein, wenn Sie das Lay- out aller Seiten oder eine Information geändert haben, die auf allen Seiten einge- bunden ist. Zend_Cache kennt verschiedene Möglichkeiten, um Inhalte zu löschen. Am ein- fachsten ist folgender Aufruf: $cache->clean(Zend_Cache::CLEANING_MODE_ALL); Diese Zeile löscht alle Daten, die sich momentan im Cache befinden. Die Methode cleanO kann übrigens auch nur veraltete Cache-Einträge löschen, sofern Sie sie mit der Konstante Zend_Cache: :CLEANING_MODE_OLD aufrufen. Damit löschen Sie die Daten recht rigoros. Allerdings haben Sie auch die Mög- lichkeit, gezielter zu löschen, was allerdings ein wenig Planung voraussetzt. Und zwar können Sie beim Abspeichern der Daten mittels save() als letzten Parame- ter ein Array mit sogenannten Tags übergeben. Hierbei handelt es sich um frei definierbare Strings, mit denen Sie Cache-Einträge zu Gruppen zusammenfassen, wenn Sie die Daten beispielsweise so ablegen: $cache->save(tdaten, Sid. array('news’, 'politik')); Die Daten dieses Eintrags sind jetzt mit den Tags news und politik versehen, wobei hier noch weitere Tags angegeben werden könnten. Möchten Sie jetzt alle Cache-Einträge löschen, die mit dem Tag news versehen sind, so rufen Sie clean() auf: $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('news')): Die Tags (auch wenn es sich hier nur um ein Tag handelt) müssen in Form eines Arrays übergeben werden. Mit der Konstante Zend_Cache: :CLEANING_MODE_ MATCH ING_TAG wird definiert, dass alle Einträge gelöscht werden, die mit diesem Tag versehen sind. Wollen Sie alle löschen, die nicht mit diesem bzw. diesen Tags versehen sind, dann übergeben Sie die Konstante Zend_Cache: :CLEANING_MODE_ NOT_MATCHING_TAG als ersten Parameter. 153
e-bol.net 4 | Infrastruktur-Klassen Mit den Tags haben Sie die Möglichkeit auf einfache Weise Abhängigkeiten zwi- schen bestimmten Teilen des Caches darzustellen. Wird einer der Teile von dem andere Teile abhängen ungültig, dann können Sie die anderen Informationen direkt mit löschen lassen. Sollte Ihnen das immer noch zu ungenau sein, so können Sie einen Eintrag auch anhand seiner ID löschen. Diese übergeben Sie an die Methode remove(), die den Löschvorgang vornimmt. 4.2 Prüfen von Werten mit Zend_Validate Daten, die Sie aus einer nicht vertrauenswürdigen Quelle wie einem Webformu- lar, einer Webschnittstelle oder einer Datei übernehmen, müssen häufig validiert werden. Am bekanntesten ist hierbei sicherlich die Prüfung von E-Mail-Adres- sen. Meist beschränkt sich die Prüfung einer E-Mail-Adresse allerdings darauf, ob der Benutzer ein ©-Zeichen eingegeben hat. Dies ist natürlich nicht ausreichend! Zend_Val i date stellt eine recht stattliche Anzahl von Klassen zur Verfügung, mit denen Sie solche und ähnliche Probleme lösen. Die Nutzung ist recht einfach. Um einen Variableninhalt zu validieren, leiten Sie zunächst ein Objekt der benötigten Klasse ab. Jede Klasse kennt eine Methode namens i s Va 1 i d (), welche den Wert übergeben bekommt. Sie liefert true oder fal se zurück, um Ihnen mitzuteilen, ob der Variableninhalt den Vorgaben entspricht. Sollte das nicht der Fall sein, können Sie mithilfe der Methode getMessagesI) die Fehlermeldung(en) ausle- sen. Sie erhalten ein Array zurück, sodass Sie die Methode auch direkt in eine foreach-Schleife integrieren können. Alternativ können Sie die Werte auch statisch prüfen lassen, was aber den Nach- teil hat, dass Sie keine Fehlermeldungen abfragen können. Ich werde hierauf nicht eingehen. Möchten Sie überprüfen, ob ein Wert eine gültige Integer-Zahl darstellt, könnte die Prüfung wie folgt aussehen: requi re_once('Zend/Validate/Int.php'): $wert = "12a3"; Svalidator = new Zend_Validate_Int(); if ($validator->isValid($wert)) { echo "Es ist ein Integer-Wert": } 154
e-bol.net Prüfen von Werten mit Zend_Validate | 4-2 el se I echo "FehlerlCbr />": foreach ($validator->getMessages() as $message) { echo ”$message<br />"; } Listing 4.7 Prüfen eines Werts mit Zend_Validate_lnt Der Wert, der hier geprüft werden soll, ist natürlich keine Integer-Zahl. Die Aus- gabe des Scripts sehen Sie in Abbildung 4.1. rs O 0 http://127.0.0.1/~c.../validator/eins.php CD OuOO______________________________________> j Camino Info .News Mac News Tabs |Q|Coogie |t A Paypal Fehler! ' 12a3' does not appear to be an integer Abbildung 4.1 Ausgabe des Scripts Wie Sie sehen, liefert das System eine eigene Fehlermeldung auf Englisch. Diese können Sie an Ihre Erfordernisse anpassen bzw. übersetzen. Um die Meldungen anzupassen, ist die Methode setMessage() vorgesehen. Sie erhält als ersten Para- meter die Fehlermeldung übergeben und als zweiten eine Konstante, die defi- niert, für welchen Fehler die Meldung »zuständig« ist. Bei der Fehlermeldung können Sie den Platzhalter %val ue% verwenden, der dann bei der Ausgabe durch den geprüften Wert ersetzt wird. Die Konstanten sind von Validator zu Validator verschieden. Sie finden sie jeweils bei der Beschreibung der Klasse. Um die Feh- lermeldung im obigen Beispiel anzupassen, könnten Sie diesen Befehl einfügen: $validator->setMessage("’%value%' ist kein Integer-Wert", Zend_Validate_Int::NOT_INT); Alternativ ist auch noch die Methode setMessages() vorgesehen, welche ein Array übergeben bekommt. Hierbei ist die Konstante der Schlüssel und die dazu- gehörige Meldung der Wert. Abhängig von der Validierungsklasse können auch noch weitere Platzhalter neben dem für den Wert möglich sein. Nachfolgend finden Sie die einzelnen Klassen beschrieben. 155
e-bol.net 4 | Infrastruktur-Klassen 4.2.1 Prüfen auf alphanumerische Daten Die Klasse Zend_Validate_Alnum gibt Ihnen die Möglichkeit zu überprüfen, ob ein übergebener String aus alphanumerischen Zeichen besteht. Zusätzlich kön- nen Sie dem Konstruktor einen booleschen Wert übergeben, welcher definiert, ob die Überprüfung Whitespaces akzeptieren soll. Bitte beachten Sie, dass deut- sche Sonderzeichen nicht als alphanumerische Zeichen angesehen werden. Konstante Bedeutung N0T_ALNUM Der übergebene Wert ist nicht alphanumerisch. $TRING_EMPTY Es wurde ein Leerstring übergeben. 4.2.2 Prüfen von Texten Die Klasse Zend_Val idate_Al pha ist weitgehend identisch mit Alnum, mit dem Unterschied, dass nur alphabetische Zeichen - derzeit allerdings ohne deutsche Sonderzeichen - akzeptiert werden. Konstante Bedeutung N0T_ALPHA Der übergebene Wert ist nicht alphanumerisch. $TRING_EMPTY Es wurde ein Leerstring übergeben. 4.2.3 Prüfen, ob eine Zahl in einem bestimmten Bereich liegt Der Validator Zend_Val idate_Between gibt Ihnen die Möglichkeit zu testen, ob ein Wert zwischen zwei anderen liegt. Der Konstruktor akzeptiert drei Werte: die untere Grenze, die obere Grenze sowie einen booleschen Wert, der definiert, ob der Wert gleich den Grenzwerten sein darf oder größer bzw. kleiner sein muss. Sie können die Werte auch über die Methoden setMi n (), setMax() und set I n - clusi ve() setzen, falls Sie dies bevorzugen. Die aktuellen Einstellungen können Sie mithilfe der »Umkehrmethoden«, deren Namen jeweils mit get beginnen, wieder auslesen. Für die Fehlermeldungen stehen bei dieser Klasse die Platzhal- ter %mi n% und %max% zur Verfügung, welche bei der Ausgabe durch den minima- len und den maximalen Wert ersetzt werden. Konstante I Bedeutung NOT_BETWEEN NOT_ BETWEEN_STRICT 156 Der übergebene Wert ist nicht größer oder gleich bzw. kleiner oder gleich den Grenzen. Der zu testende Wert ist nicht echt größer bzw. echt kleiner als die Grenzen.
e-bol.net Prüfen von Werten mit Zend_Validate | 4.2 4.2.4 Prüfen von Kreditkartennummern Zend_Validate_Ccnum überprüft eine Kreditkartennummer auf ihre Gültigkeit hin. Genau genommen wird nicht wirklich die Gültigkeit geprüft, sondern nur die Plausibilität. Das heißt, es wird geprüft, ob in der übergebenen Zahl die kor- rekten Prüfziffern enthalten sind und die Länge der Zahl stimmt. Möchten Sie Kreditkartenzahlungen abwickeln, so ist eine zusätzliche Prüfung der Kreditkar- tendaten dringend erforderlich, was aber meistens durch den Dienstleister erfolgt, der die Zahlungsabwicklung für Sie übernimmt. Die Klasse benötigt keine weiteren Informationen, ebenso erwartet der Kon- struktor keine Daten. Bitte achten Sie darauf, die Kreditkartennummer als String ohne Leerzeichen zu übergeben. Konstante Bedeutung LENGTH Die übergebene Zahl hat nicht die korrekte Länge. CHECKSUM Bei der Prüfung ist ein Prüfsummenfehler aufgetreten. 4.2.5 Prüfen eines Datums Bei der Klasse Zend_Val i date_Date liefert die Methode i sVal i d() nur dann den Wert true, falls ein gültiges Datum im Format JJJJ-MM-TT übergeben wurde. Konstante Bedeutung NOT_YYYY_MM_DD Das Datum scheint das falsche Format zu haben. INVALID Das Datum ist nicht gültig. 4.2.6 Testen von Ziffernfolgen Mithilfe von Zend_Val idate_Di gi ts können Sie prüfen, ob ein String nur aus Ziffern besteht. Konstante Bedeutung $TRING_EMPTY Es wurde ein leerer String zur Prüfung übergeben. NOT_DIGITS Der String besteht nicht nur aus Ziffern. 4.2.7 Validieren von E-Mail-Adressen Eine E-Mail-Adresse auf ihre Gültigkeit hin zu prüfen, ist sicher eine der größe- ren Herausforderungen bei der Validierung von Formulardaten. Hierbei liefert Zend_Val idate_Emai 1 Address eine gute Hilfestellung. 157
e-bol.net 4 | Infrastruktur-Klassen Die Klasse ist nicht nur in der Lage, eine E-Mail-Adresse auf syntaktische Richtig- keit hin zu prüfen, sondern kann auch prüfen, ob es für die Domain überhaupt einen MX-Eintrag1 gibt. Eine Prüfung, ob es die E-Mail-Adresse auf dem entspre- chenden Server gibt, ist nicht vorgesehen. Dies ist meist auch nicht sinnvoll, da der größte Teil der Mail-Server diese Überprüfung nicht zulässt, um das Spam- Aufkommen zu reduzieren. Im einfachsten Fall leiten Sie einfach ein Objekt der Klasse ab und übergeben der Methode isVal id() die E-Mail-Adresse, die geprüft werden soll. In diesem Fall wird dann lediglich geprüft, ob die Syntax der Adresse gültig ist. Dabei sind aller- dings nur Hostnames zulässig, die per DNS aufgelöst wurden. Um auch lokale Hostnames bzw. IP-Adressen zuzulassen, können Sie Konstanten der Klasse Zend_ Val idate_Hostname an den Konstruktor übergeben. Die Erläuterung der Kon- stanten ALLOW_DNS, ALLOW_IP und ALLOW_LOCAL finden Sie in Abschnitt 4.2.11. Mit dem zweiten Parameter - dabei handelt es sich um einen booleschen Wert - legen Sie fest, dass die Klasse prüft, ob es einen MX-Eintrag zu der übergebenen Domain gibt. Der dritte Parameter erlaubt Ihnen, ein Objekt der Klasse Zend_Validate_Host- name zu übergeben. Sollten Sie für eine andere Validierung schon ein Objekt die- ser Klasse abgeleitet haben, ist es nicht sinnvoll, dies an dieser Stelle zu überge- ben. Damit muss nicht noch ein weiteres Objekt der Klasse abgeleitet werden, wodurch Ressourcen gespart werden. Das Hostname-Objekt wird intern in der Eigenschaft hostnameValidator abge- legt. Möchten Sie noch Methoden des Objekts aufrufen, weil Sie beispielsweise IDN-Domains nutzen wollen, so können Sie dies über diese Eigenschaft tun. Möchten Sie prüfen, ob es einen MX-Eintrag zu der fraglichen Domain gibt, setzt dies voraus, dass die Funktion dns_get_mx() vorhanden ist. Ist das nicht der Fall, wirft die Methode isVal id() eine Exception. Wollen Sie vorher prüfen, ob die Funktion vorhanden ist, können Sie die Methode val idateMxSupported() aufru- fen, die true zurückliefert, falls die Funktion auf dem System vorhanden ist. Lei- der kann diese Methode nicht statisch aufgerufen werden. Sie sollten daher zuerst ein Objekt ableiten, um mit diesem zu testen, ob MX-Einträge geprüft wer- den können. Anschließend lässt sich mithilfe der Methode setVal idateMxl) die Einstellung ändern. Bitte beachten Sie, dass es zurzeit noch nicht möglich ist, den MX-Eintrag von IDN-Domains zu prüfen. 1 MX steht für Mail Exchange. Weitere Informationen finden Sie unter der Adresse http://de.wikipedia.org/wiki/MX_record. 158
e-bol.net Prüfen von Werten mit Zend_Validate | 4-2 requi re_once('Zend/Vali date/Emai1 Add ress.php'); Semail = 'postmaster@moehrke.de'; // Wir akzeptieren Hostnames und IPs Sallow = Zend_Validate_Hostname::ALLOW_DNS | Zend_Validate_Hostname::ALLOW_IP; // Neues Objekt ableiten Svalidator = new Zend_Validate_Emai1Addressl$al1ow); $mx_validieren = false; // Können MX-Einträge geprüft werden? if ($validator->vali dateMxSupported()) { $mx_validieren = true; } // MX-Einstellung setzen $validator->setVali dateMx($mx_validi eren); // IDN-Prüfung ausschalten // Entspricht dem Default. Aufruf nur als Beispiel $val i dator->hostnameValidator->setValidateldn(false); if ($validator->isValid($emai1)) { echo "E-Mai 1-Adresse korrekt!"; } el se { echo "E-Mai 1-Adresse nicht akzeptiert!<br>"; foreach ($validator->getMessages() as Smessage) echo "$message<br>"; 1 } Listing 4.8 Validieren einer E-Mail-Adresse Für die Fehlermeldungen können Sie bei dieser Klasse die Platzhalter %local - Part% und %hostname% nutzen, welche bei der Ausgabe durch den lokalen Teil der E-Mail-Adresse bzw. den Hostname ersetzt werden. Konstante Bedeutung INVALID Die E-Mail-Adresse ist nicht korrekt. INVALID_HOSTNAME Der Hostname-Teil der E-Mail-Adresse ist nicht korrekt. INVALID_MX_RECORD Es konnte kein korrekter MX-Eintrag für die E-Mail-Adresse ermittelt werden. 159
e-bol.net 4 | Infrastruktur-Klassen Konstante Bedeutung DOT_ATOM Der lokale Teil der E-Mal-Adresse besitzt kein korrektes Dot-Atom-Format. QUOTED_STRING Der lokale Teil der E-Mail-Adresse liegt nicht in einem korrekten Quoted-String-Format vor. INVALID_LOCAL_PART Der lokale Teil der E-Mail-Adresse ist ungültig. 4.2.8 Testen eines Strings auf Fließkomma-Eigenschaften Die Klasse Zend_Val i date_Fl oat prüft, ob ein übergebener Wert eine Fließkom- mazahl enthält. Hierbei erkennt die Methode i sVal id() sowohl Zahlen in der üblichen Dezimalschreibweise (z.B. 12.30) als auch in der Exponentialschreib- weise (z.B. 12e3). Hierbei ist zu beachten, dass sich bei dieser Klasse auch die Lokalisierungseinstellungen auswirken. Wurde beispielsweise de_DE als Lokali- sierung eingestellt, werden auch Zahlen akzeptiert, bei denen der Nachkomma- anteil mit einem Komma und nicht mit einem Dezimalpunkt abgetrennt ist. Konstante Bedeutung NOT_FLOAT Es handelt sich nicht um eine Fließkommazahl. 4.2.9 Prüfen, ob eine Zahl über einer Grenze liegt Mithilfe des Validators Zend_Val idate_GreaterThan testen Sie, ob ein Wert grö- ßer als eine Grenze ist. Der Grenzwert kann direkt an den Konstruktor übergeben oder nachträglich mit setMi n() gesetzt werden. Um die Grenze auszulesen, steht getMi n() zur Verfü- gung. Für die Fehlermeldung steht in diesem Fall der Platzhalter %mi n% zur Verfügung, welcher bei der Ausgabe durch den Grenzwert ersetzt wird. Konstante Bedeutung NOT_GREATER Der Wert ist nicht größer als die Grenze. 4.2.10 Testen von hexadezimalen Zahlen Die Methode isVal id(), die in Zend_Val idate_Hex definiert ist, liefert nur dann true, wenn der übergebene String Zeichen enthält, die hexadezimale Ziffern darstellen. Er darf also nur aus den Ziffern von 0 bis 9 sowie den Buchstaben von A-Z in Groß- oder Kleinschrift bestehen. 160
e-bol.net Prüfen von Werten mit Zend_Validate | 4-2 Konstante Bedeutung NOT_HEX Der Wert besteht nicht nur aus hexadezimalen Ziffern. 4.2.11 Validieren von Hostnames Die Klasse Zend_Val i date_Hostname ist ein sehr interessanter Validator. Mit ihm können Sie prüfen, ob ein übergebener String einen gültigen Hostname darstellt. Diese Klasse erspart Ihnen viel Arbeit, da es recht aufwändig ist, eine solche Prü- fung manuell zu implementieren. Die Klasse kann hierbei drei Arten von Anga- ben prüfen. Zum ersten sind das »normale Hostnames«, deren Name über DNS aufgelöst wird, zum zweiten IP-Adressen und zum dritten werden auch lokale Namen wie beispielsweise l ocal host unterstützt. Die Information, welche Arten von Hostnames zulässig sind, wird dem Konstruk- tor als erster Parameter übergeben. Die Klasse kennt dafür die drei Konstanten ALLOW_DNS, ALLOW_IP und ALLOW_LOCAL. Wenn Sie also nur IPs als Hostname zulassen wollen, so übergeben Sie dem Konstruktor als ersten Parameter Zend_ Val i date_Hostname: :ALLOW_IP. Möchten Sie mehrere Möglichkeiten kombinie- ren, können Sie die verschiedenen Konstanten einfach mit einem |-Operator (Bit- Oder-Operator) verbinden. Möchten Sie alle Varianten zulassen, können Sie auch einfach die Konstante ALLOW_ALL nutzen. Der Default-Wert an dieser Stelle ist ALLOW_DNS. Mit einem booleschen Wert, der als zweiter Parameter übergeben wird, lässt sich festlegen, ob IDN-, also beispielsweise sogenannte »Umlaut-Domains«, zulässig sein sollen. Standardmäßig ist der Wert auf true gesetzt, was bedeutet, dass sie akzeptiert werden. Bitte beachten Sie dabei, dass die Hostnames in diesem Fall in UTF-8-Codierung übergeben werden müssen. Der dritte Parameter, auch ein boolescher Wert, der standardmäßig true lautet, definiert, ob die Top Level Domain geprüft werden soll. Das heißt, die Klasse ver- fügt über eine Liste mit allen momentan gültigen Top Level Domains und ver- gleicht den übergebenen Wert mit dieser Liste. Damit ist sichergestellt, dass keine Domains übergeben werden können, die nicht existieren. Sie müssen aller- dings darauf achten, dass das Zend Framework aktualisiert wird, damit die Liste der Domains stets komplett ist. Sollten Sie schon ein Objekt der Klasse Zend_Validate_Ip abgeleitet haben, so können Sie dieses als vierten Parameter übergeben. Dies dient nur dazu, unnö- tige Instanzen der Klasse zu verhindern, da sonst intern ein Objekt dieser Klasse abgeleitet wird. 161
e-bol.net 4 | Infrastruktur-Klassen Auch bei dieser Klasse gilt, dass Sie die Einstellungen, die Sie dem Konstruktor übergeben, auch noch nachträglich setzen können. Dazu sind die Methoden setAllowO, setVal idateldn(), setVal idatenTld() und setlpVal idator() vorgesehen. Ein nachträgliches Übergeben eines IP-Validators hat allerdings wenig Sinn, da das entsprechende Objekt direkt bei der Instantiierung des Zend_ Val idate_Hostname-Objekts abgeleitet wird. Ein wenig erstaunlich ist, dass zur- zeit nur eine Funktion zum Auslesen der Einstellungen, nämlich getAllowO, vorhanden ist. require_once('Zend/Validate/Hostname.php'); $host = utf8_encode("www.möhrke.de"); // DNS und IP werden akzeptiert Sallow = Zend_Validate_Hostname::ALLOW_DNS | Zend_Vali date_Hostname::ALL0W_IP; Svalidator = new Zend_Validate_Hostname($al1ow, true, //IDN ist OK true); // TLD prüfen if ($ validator->i sValid($host)) { echo "Domain akzeptiert"; } el se { echo "Fehl er I<br>"; foreach ($validator->getMessages() as Smessage) echo "$message<br>"; 1 } Listing 4.9 Prüfen von Hostnames Konstante Bedeutung IP_ADDRESS_NOT_ALLOWED Es wurde eine IP-Adresse übergeben, obwohl diese nicht zugelassen ist. UNKNOWN_TLD Die Prüfung von Top Level Domains ist eingeschaltet und der Hostname enthält eine unbekannte Top Level Domain. INVALID_DASH Der Hostname enthält ein Dash (-) an einer unzulässigen Position. INVALID_HOSTNAME_SCHEMA Der Hostname ist nach einem nicht zulässigen Schema aufgebaut. UNDECIPHERABLE_TLD Die Top Level Domain kann nicht extrahiert werden. 162
e-bol.net Prüfen von Werten mit Zend_Validate | 4-2 Konstante Bedeutung INVALID_HOSTNAME ungültiger Hostname INVALID_LOCAL_NAME ungültiger Local Name LOCAL_NAME_NOT_ALLOWED Es wurde ein lokaler Name übergeben, was allerdings nicht zulässig ist. 4.2.12 Testen von Array-Inhalten Bei der Klasse Zend_Val i date_InArray handelt es sich primär um einen Wrapper für die Funktion in_array(). Sie können somit mithilfe dieser Klasse ein Array darauf hin testen, ob ein bestimmter Wert enthalten ist. Der Konstruktor bekommt das Array übergeben, das durchsucht werden soll. Als zweiten Parame- ter können Sie einen booleschen Wert übergeben. Handelt es sich dabei um true, wird nicht nur der Wert, sondern auch der Datentyp geprüft. Bei einem fal se, was den Default-Wert darstellt, wird der Datentyp nicht getestet. Möchten Sie die Parameter erst nach dem Ableiten des Objekts setzen, so können Sie die Methode setHaystack() zum Setzen des Arrays und setStrictt) zum Festlegen des zweiten Parameters benutzen, wobei es auch hier wieder Metho- den gibt, welche die entsprechenden Werte auslesen. Sie heißen getHaystack() und getStrictt). Der gesuchte Wert wird dann der Methode i sVal i d() übergeben. Konstante Bedeutung NOT_IN_ARRAY Der gesuchte Wert ist nicht in dem Array enthalten. 4.2.13 Validieren von Integer-Werten Die Methode isValidO der Klasse Zend_Val idate_Int liefert true, wenn der übergebene Wert eine Integer-Zahl darstellt. Hierbei darf die Zahl auch als Daten- typ String übergeben werden. Konstante Bedeutung NOT_INT Der übergebene Wert ist keine Integer-Zahl. 4.2.14 Prüfen von IP-Adressen Mit der Klasse Zend_Validate_Ip können Sie IP-Adressen validieren. Zurzeit kann die Klasse nur mit IPv4-Adressen umgehen, da im Hintergrund auf die Funktion i p2l ong() zugegriffen wird. 163
e-bol.net 4 | Infrastruktur-Klassen Konstante Bedeutung NOT_IP_ADDRESS Es handelt sich bei dem übergebenen Wert nicht um eine gültige IP-Adresse. 4.2.15 Prüfen, ob eine Zahl unter einer Grenze liegt Mithilfe von Zend_Val idate_LessThan können Sie testen, ob der zu prüfende Wert kleiner als ein Grenzwert ist. Der Grenzwert wird hierbei direkt an den Konstruktor übergeben. Der Grenzwert kann, nachdem das Objekt abgeleitet wurde, mithilfe von set- Max() verändert und mit getMax() ausgelesen werden. Für die Fehlermeldung können Sie zusätzlich den Platzhalter %max% nutzen. Konstante I Bedeutung NOT_LESS Der übergebene Wert ist nicht kleiner als die Grenze. 4.2.16 Testen, ob eine Variable leer ist Zend_Val idate_NotEmpty testet, ob eine übergebene Variable nicht leer ist. Bitte beachten Sie, dass die Funktion empty(), die im Hintergrund arbeitet, den String " 0" als leer ansieht. Konstante Bedeutung IS.EMPTY Die übergebene Variable ist leer. 4.2.17 Validierung auf Basis eines regulären Ausdrucks Diese Klasse Zend_Val idate_Regex bekommt bei der Objektinstantiierung einen regulären Ausdruck in PCRE-Syntax übergeben. Die Methode isValidO prüft dann, ob der reguläre Ausdruck einen Treffer mit dem übergebenen Wert liefert. Nachdem Sie das Objekt abgeleitet haben, können Sie den regulären Ausdruck ändern, indem Sie die Methode setPattern() aufrufen und ihr den neuen Aus- druck übergeben. Sie können den »gerade aktuellen« Ausdruck übrigens mit get- PatternO auslesen. Bitte beachten Sie, dass die Klasse beim Verwenden eines ungültigen regulären Ausdrucks eine Exception wirft. Möchten Sie bei der Fehlermeldung den regulären Ausdruck mit ausgeben, kön- nen Sie dafür den Platzhalter %pattern% nutzen. Konstante Bedeutung N0T_MATCH Der reguläre Ausdruck lieferte keinen Treffer. 164
e-bol.net Filtern von Daten mit Zend_Filter | 4-3 4.2.18 Testen eines Strings auf seine Länge hin Möchten Sie einen String auf seine Länge hin überprüfen, so ist Zend_Val idate_ StringLength Ihr Freund. Der Konstruktor der Klasse kann eine Mindest- und eine Höchstlänge übergeben bekommen. Beide Werte sind optional und können auch später mit den Methoden setMi n() und setMax() gesetzt werden. Die aktu- ellen Parameter können Sie mit getMin() und getMax() auslesen. Für die Fehlermeldung stehen bei diesem Paket noch die Platzhalter %mi n% und %max% zur Verfügung, welche dann durch die Unter- bzw. Obergrenze ersetzt werden. Konstante Bedeutung TOO_SHORT Der übergebene String ist zu kurz. TOO_LONG Der übergebene String ist zu lang. 4.3 Filtern von Daten mit Zend_Filter Wenn Sie Daten aus Datenbanken, Formularen oder Webschnittstellen überneh- men, werden Sie schnell damit konfrontiert, dass Sie diese Daten filtern oder konvertieren müssen. Dabei kann es darum gehen, Daten zu vereinheitlichen, Sonderzeichen zu konvertieren oder potenziell gefährliche Zeichen zu entfernen. PHP kennt hierfür schon eine ganze Menge nützlicher Funktionen, auf die Zend_ Filter aufsetzt. Der große Vorteil von Zend_Filter besteht darin, dass die Klasse ein einheitliches Interface bietet und erweiterungsfähig ist. Sie können also die Klasse um eigene Filter ergänzen, die dann genau wie die bereits vorge- fertigten Filter genutzt werden. Bevor ich auf die Klasse(n) eingehe, noch ein Wort der Warnung. Ein Filter kann nur dann korrekt arbeiten, wenn Art und Struktur der eingehenden Daten bekannt sind. Bitte beachten Sie also immer, dass die Lokalisierungseinstellungen korrekt sind, und dass Sie dem Filter mitteilen, in welchem Zeichensatz die Daten vorliegen. Die Funktionalität der Klasse können Sie auf zwei Wegen nutzen. Zum ersten können Sie ein Objekt eines speziellen Filters ableiten. Die zweite Möglichkeit ist ein statischer Aufruf des Filters mithilfe der Methode get(), welche die Daten für Sie konvertiert. In den meisten Fällen wird es aber sicher eher von Vorteil sein, ein Objekt abzuleiten und mit diesem zu arbeiten, da Sie es für mehrere Konvertierungsvorgänge nutzen können. 165
e-bol.net 4 | Infrastruktur-Klassen Die einzelnen Filter-Klassen finden sich im Unterverzeichnis Filter unterhalb des Zend-Ordners. Der Name der Klassendatei entspricht dabei dem Namen des jeweiligen Filters. Wenn Sie also beispielsweise den Filter Html Entities benöti- gen, müssen Sie die Klassendatei Zend/Filter/HtmlEntities.php inkludieren. Jede der Filter-Klassen verfügt über eine Methode f i 1 ter(), mit der Sie den Fil- ter anwenden können. Sie bekommt die Daten übergeben, konvertiert sie und gibt sie danach zurück: requi re_once 'Zend/Fi 1 ter/HtmlEntities.php'; Sfilter = new Zend_Fi1ter_HtmlEntities(): echo $fi1ter->fi1ter('Süße Soße'): //Ausgabe: S&uuml:&szlig:e So&szlig:e Der vergleichbare statische Aufruf sähe so aus: require_once 'Zend/Filter.php': echo Zend_Filter::get('Süße Soße', 'HtmlEntities'): Bei dem Aufruf der Methode get() übergeben Sie den zu filternden Text als ers- ten Parameter und den Namen des Filters an zweiter Stelle. Sollte der Konstruk- tor der eigentlichen Klasse noch weitere Parameter akzeptieren, dann können Sie diese nach dem Namen angeben. Bitte beachten Sie, dass Sie für den statischen Aufruf die Klasse Zend_Fi 1 ter ein- binden und nutzen müssen. Diese sorgt dafür, dass andere benötigte Klassen inkludiert werden. In vielen Fällen wird es nötig sein, dass Sie Daten durch mehrere Filter schicken. Um solche Fälle zu vereinfachen, sind »Chains« vorgesehen. Eine Chain ist nichts anderes als eine Aneinanderreihung von Filtern, die ein Datensatz durchläuft, bevor Sie die fertig konvertierten Daten erhalten. Eine solche Chain ist ein Objekt der Klasse Zend_Fi 1 ter, bei dem die verschiede- nen Filter mithilfe von addFi 1 ter() angemeldet werden. Wollen Sie bei einer Eingabe beispielsweise die Sonderzeichen durch Entitäten ersetzen und gleich- zeitig sicherstellen, dass alle Buchstaben in Kleinschrift dargestellt werden, dann ist das kein Problem. Neben dem Filter Html Enti ti es wäre in diesem Fall noch der Filter StringToLower beteiligt: require_once 'Zend/Filter.php': requi re_once 'Zend/Fi 1 ter/HtmlEntities.php'; requi re_once 'Zend/Fi 1ter/StringToLower.php’: Schain = new Zend_Fi1ter(): 166
e-bol.net Filtern von Daten mit Zend_Filter | 4-3 $f11ter_entity = new Zend_Fi1ter_HtmlEntities(); $fi 1 ter_lower = new Zend_Fi 1 ter_Stri ngTol_ower(); $chain->addFilter($filter_enti ty) ->addFi1ter($fi1ter_lower); echo $chain->fi1ter(’ÄÖÜ'); // Ausgabe: &auml;&ouml;&uuml: Wie auch in den meisten anderen Klassen ist auch hier wieder ein »fluent Inter- face« umgesetzt, sodass sich verschiedene addFi 1 ter ()-Aufrufe miteinander ver- knüpfen lassen. Eine Kleinigkeit möchte ich an dieser Stelle noch anmerken. Das obige Beispiel funktioniert nur, weil die Umlaute zuerst in Entitäten konvertiert und dann in Kleinbuchstaben umgesetzt werden. Somit wird aus einem Ä zuerst ein &Auml; bei welchem dann das A in ein a konvertiert werden kann. Da hier nicht explizit angegeben ist, mit welcher Lokalisierung gearbeitet wird, würde das System die deutschen Umlaute nicht als Buchstaben erkennen. Möchten Sie sicherstellen, dass die Filter immer korrekt funktionieren, so achten Sie bitte darauf, dass die korrekte Lokalisierung gesetzt ist: setl ocale(LC_ALL,’de_DE'); Die Verwendung der Klasse Zend_Loca 1 e ist an dieser Stelle noch nicht vorgesehen. Wie Sie schon in den Beispielen gesehen haben, kennt Zend_Filter eine ganze Reihe vorgefertigter Filter, die direkt eingesetzt werden können. Jede der Filter- Klassen kennt die Methode filtert), welche die ihr übergebenen Daten ent- sprechend den Vorgaben bearbeitet. Nachfolgend finden Sie eine Beschreibung der vordefinierten Klassen. 4.3.1 Alphanumerische Zeichen mit Zend_Filter_Alnum filtern Der Filter Ain um akzeptiert nur Buchstaben und Zahlen. Alle anderen Zeichen werden aus dem String entfernt. Optional können Sie dem Konstruktor noch true übergeben, sofern Sie auch Whitespaces akzeptieren wollen. Bitte beachten Sie, dass nicht nur Leerzeichen, sondern alle nicht druckbaren Zeichen wie zum Beispiel Zeilenumbrüche zu den Whitespaces gehören. Diese Tatsache kann schnell ein Sicherheitsproblem nach sich ziehen. Dieser Filter ist - wie ein paar andere Filter auch - mit Vorsicht zu genießen. Momentan ist die Funktionsweise folgende: Die Klasse prüft, ob die installierte PHP-Version bei der Verarbeitung von regulären Ausdrücken Unicode unter- stützt. Abhängig von dieser Überprüfung nutzt das System unterschiedliche regu- läre Ausdrücke, was dazu führt, dass auf Systemen ohne Unicode-Unterstützung 167
e-bol.net 4 | Infrastruktur-Klassen keine Umlaute akzeptiert werden, wohingegen Systeme mit Unicode-Unterstüt- zung Umlaute akzeptieren. Ein etwas unschönes Verhalten, wie ich finde.2 Um zu prüfen, ob Ihre PHP-Installation mit Unicode-Unterstützung für die PCRE- Engine erstellt wurde, können Sie die nachfolgende i f-Abfrage benutzen: if(l =—@preg_match('/\pL/u’, ’a')) { // Ja, mit Unicode 1 el se { // Nein, ohne Unicode 1 Listing 4.10 Testen der UTF-8-Unterstützung der PCRE-Engine Unterstützt Ihr System Unicode, und wollen Sie sichergehen, dass Umlaute akzeptiert werden, so muss der zu prüfende Text im UTF-8-Format vorliegen. 4.3.2 Buchstaben filtern Der Filter Zend_Fi l ter_Al pha akzeptiert nur Buchstaben. Ansonsten ist sein Ver- halten identisch mit dem von Al num. 4.3.3 Extrahieren eines Basenames Bei der Klasse Zend_Fi l ter_BaseName handelt es sich um einen Wrapper für die PHP-Funktion basenameO. Die Methode filter() bekommt einen Dateipfad inklusive des Dateinamens übergeben, extrahiert den Namen der Datei und lie- fert diesen zurück. Als Dateiname wird hierbei der letzte Teil des übergebenen Pfads angesehen. Es kann sich dabei auch um den Namen eines Unterverzeichnis- ses handeln: requi re_once 'Zend/Filter/BaseName.php'; Sfilter = new Zend_Filter_BaseName(): // Gibt index.php aus echo $filter->filter('/va r/www/htdocs/index.php’); // Gibt htdocs aus echo $filter->filter('/var/www/htdocs/'); Listing 4.11 Basename extrahieren 2 Bitte prüfen Sie beim Einsatz dieser Klasse, ob sich dieses Verhalten geändert hat. 168
e-bol.net Filtern von Daten mit Zend_Filter | 4-3 4.3.4 Ziffern mit Zend_Filter_Digits ausfiltern Der Filter Digits akzeptiert ausschließlich Ziffern; alle anderen Zeichen werden entfernt. Auch wenn Ziffern im Unicode und im ISO-Zeichensatz identisch darge- stellt werden, so unterscheidet die Klasse intern doch, ob Unicode-Unterstützung vorhanden ist. Ist keine Unicode-Unterstützung vorhanden, so wird der reguläre Ausdruck [A0-9] genutzt, wohingegen bei Unicode-Unterstützung [\p(AN} ] genutzt wird. Die Ausdrücke sollten sich identisch verhalten. Sollte es aber doch einmal zu einem unerwarteten Verhalten kommen, so prüfen Sie bitte im PHP- Change-Log, ob sich das Verhalten der zweiten Variante eventuell geändert hat. 4.3.5 Extrahieren von Verzeichnisnamen Zend_Fi 1 ter_Di r stellt einen Wrapper für die PHP-Funktion dirname() dar. Übergeben Sie der Methode einen Pfad inklusive Dateinamen, so liefert sie den eigentlichen Pfad ohne Dateinamen zurück. Es handelt sich also um das Gegen- stück zum Filter BaseName. Es wird also der letzte Teil des übergebenen Pfads ent- fernt. Sollte es sich dabei nicht um einen Dateinamen handeln, kann die Klasse dies nicht erkennen. requi re_once 'Zend/Fi 1 ter/Di r.php'; Sfilter = new Zend_Fi1ter_Dir(); // Ausgabe: /var/www/htdocs echo $fi1ter->fi1ter('/var/www/htdocs/index.php’); // Ausgabe: /var/www echo $fi1ter->fi1ter('/var/www/htdocs/'); 4.3.6 Konvertieren von Sonderzeichen in Entitäten Den Filter Zend_Fi 1 ter_Html Enti ties haben Sie schon in den Beispielen am Anfang des Kapitels kennengelernt. Er konvertiert die Sonderzeichen in einem übergebenen Text in die korrekten HTML-Entitäten. Bei dieser Klasse arbeitet im Hintergrund die PHP-Funktion htmlentitiest), wie Sie sich sicher schon den- ken. Somit akzeptiert der Konstruktor auch die selben Parameter, wie die Funk- tion sie zusätzlich zu dem String akzeptieren würde. An erster Stelle können Sie dem Konstruktor eine der Konstanten ENT_COMPAT, ENT_QUOTES oder ENT_NOQUO- TES übergeben. Die erste, die auch den Default-Wert darstellt, sorgt dafür, dass einfache Anführungszeichen nicht in Entitäten konvertiert werden. Die zweite Konstante stellt sicher, dass einfache Anführungszeichen durch die Entität &#039; ersetzt werden. Bei der Verwendung des dritten Wertes werden weder einfache noch doppelte Anführungszeichen verändert. 169
e-bol.net 4 | Infrastruktur-Klassen Als zweiten Parameter können Sie den Zeichensatz angeben, in welchem der zu filternde Text vorliegt. Hierbei ist ISO-8859-1 der Standardwert. Sollte es einmal notwendig sein, so können Sie diese beiden Werte auch nachträglich verändern bzw. auslesen, welche Werte gesetzt wurden. Hierzu sind die Methoden getCharSetO und setCharSetO bzw. setQuoteStyle() und getQuoteStyle() deklariert. 4.3.7 Filtern von Integer-Werten Der Filter Int konvertiert einen übergebenen String in einen Integer-Wert. Auch wenn das ein wenig an den Filter Digits erinnert, so unterscheiden die beiden sich doch deutlich. Di gi ts entfernt alle Nicht-Zahl-Zeichen aus einem String und gibt ihn zurück. Int hingegen führt ein Casting durch. Das heißt, der Rückgabewert ist vom Datentyp Integer, und nur die Ziffern, die am Anfang des übergebenen Strings zu finden waren, werden zurückgegeben. Aus 12a34 würde somit 12, wohingegen Digits den Wert 1234 zurückgeben würde. Zeichensätze oder Ähnliches müssen hierbei nicht beachtet werden. Der Konstruktor akzeptiert keine Parameter. 4.3.8 Absolute Pfade mit Zend_Filter_RealPath extrahieren Mithilfe des Filters Real Path können Sie eine relative Pfadangabe in eine abso- lute konvertieren. Für die Bestimmung des absoluten Pfads wird das aktuell genutzt Unterverzeichnis als Grundlage genutzt: requi re_once 'Zend/Fi 1 ter/Real Path.php'; Sfilter = new Zend_Fi1ter_RealPath(); // Datei liegt in: /var/www/htdocs/fi1tertest // Ausgabe: /var/www/index.php echo $fi1ter->fi1ter('../../index.php'); 4.3.9 Konvertieren in Kleinbuchstaben Zend_Fi 1 ter_StringToLower dient dazu, einen übergebenen String in Klein- buchstaben zu konvertieren. Auch an dieser Stelle sind Umlaute, die eventuell in dem String enthalten sind, nicht ganz unproblematisch. Die Klasse geht standard- mäßig davon aus, dass der übergebene String im ISO-8859-1-Zeichensatz vorliegt und nutzt die PHP-Funktion strtolower() für die Konvertierung. Sollten Sie allerdings einen anderen Zeichensatz nutzen wollen, so teilen Sie das dem Objekt dadurch mit, dass Sie den Zeichensatz an die Methode setEncoding() überge- ben. In diesem Fall wird im Hintergrund die Funktion mb_strtolower() genutzt. 170
e-bol.net Filtern von Daten mit Zend_Filter | 4-3 Das hat zur Folge, dass Sie - wenn Sie setEncodi ng() nicht aufrufen - die Loka- lisierung mit setlocale() setzen müssen, damit Umlaute erkannt werden. Im zweiten Fall ist das dann nicht mehr notwendig, da mb_strtol ower() diese auto- matisch konvertiert. Wenn Sie Umlaute also korrekt konvertieren wollen, so gibt es hierfür zwei mögliche Vorgehensweisen. Ansatz 1: setl ocale(LC_ALL,’de_DE'); requi re_once 'Zend/Fi 1ter/Stri ngToLower.php’; Sfilter = new Zend_Fi1ter_StringToLower!); echo $fi1 ter->fi1 ter('ÄÖÜ'); Ansatz 2: requi re_once 'Zend/Fi 1ter/Stri ngToLower.php’; Sfilter = new Zend_Fi1ter_StringToLower!); $fi1ter->setEncoding('ISO-8859-1'); echo $fi1 ter->fi1 ter('ÄÖÜ'); 4.3.10 Konvertieren in Großbuchstaben Das Verhalten von StringToUpper ist identisch mit dem von StringToLower, wobei der übergebene String in Großbuchstaben konvertiert wird. 4.3.11 Entfernen von Whitespaces StringTrim stellt einen Wrapper für die PHP-Funktion trim!) dar. Mithilfe die- ses Filters können Sie Whitespaces am Anfang und Ende eines Strings entfernen. Zusätzlich können Sie dem Konstruktor noch einen String übergeben, der eine Liste von Zeichen enthält, die von Beginn und Ende des Strings entfernt werden sollen. Diesen String können Sie alternativ auch nach Ableiten des Objekts mit- hilfe von setCharList() setzen oder mit getCharList!) auslesen. $ref = "http://192.168.0.23/”; requi re_once 'Zend/Filter/Stri ngTrim.php'; Sfilter = new Zend_Filter_StringTrim(); // Zeichenliste hätte auch dem Konstruktor // übergeben werden können $filter->setCharList(':/htp'); // Ausgabe: 192.168.0.23 echo $fi1 ter->fi1 ter($ref): Listing 4.12 Trimmen eines Strings 171
e-bol.net 4 | Infrastruktur-Klassen 4.3.12 Entfernen von HTML-Tags Auch der Filter Stri pTags stellt einen Ersatz für stri ptags() aus PHP dar. Aller- dings handelt es sich nicht um einen einfachen Wrapper, sondern um eine kom- plett neue Implementation, die deutlich leistungsfähiger ist. Der Filter kann Tags aus einem übergebenen String entfernen. Wie die PHP-Variante ist die Methode dabei in der Lage, bestimmte Tags beizubehalten und andere zu entfernen. Aller- dings kann der Filter auch Kommentare im Code belassen und bestimmte Attri- bute beibehalten. Diese Vielfalt an Möglichkeiten führt allerdings dazu, dass der Konstruktor ein wenig komplex in der Handhabung ist. In der einfachsten Variante übergeben Sie dem Konstruktor keine Parameter. In diesem Fall werden alle Tags, Attribute und Kommentare entfernt. Möchten Sie bestimmte Tags zulassen, übergeben Sie diese Tags in Form eines Arrays als ersten Parameter an den Konstruktor. Sollten Sie bei einigen Tags bestimmte Parameter zulassen wollen, ist dies auch über den ersten Parameter realisierbar. In diesem Fall nutzen Sie die Namen der Tags, die Sie akzeptieren möchten als Schlüssel, und übergeben die Namen der Attribute, die bei einem bestimmten Tag akzeptiert werden sollen, in Form eines Arrays als Wert an diesen Schlüssel. Sollten Sie bei einem Tag keine Attribute akzeptieren wollen, so übergeben Sie ein leeres Array. Mit dem zweiten Parameter des Konstruktors können Sie eine Liste von Attribu- ten übergeben, die bei allen Tags akzeptiert werden. Diese Liste wird einfach als Array übergeben, wobei die Namen der Attribute die Werte des Arrays darstel- len. Der dritte Parameter ist vom Typ Boolean und legt fest, ob Kommentare im Code erhalten bleiben (true) oder entfernt werden (fal se) sollen, wobei die Kommen- tare standardmäßig entfernt werden. Die folgenden Beispiele verdeutlichen die Verwendung ein wenig: Shtml = " <!-- body --> <body text='black'> Hier ist der Body <img src='bild.jpg’ width='125’ /> <iframe src='daten.php’ width='50%’ height='50%’> </i frame> </body> IT « J require_once 'Zend/Filter/StripTags.php’; 172
e-bol.net Filtern von Daten mit Zend_Filter | 4-3 // Keine Tags oder Kommentare zugelassen Sfilter = new Zend_Fi1ter_StripTags(); echo $fi1 ter->fi1 ter($html); /* Ausgabe: Hier ist der Body // Die Tags img und iframe werden zugelassen. // Alle Tags dürfen über das Attribut src verfügen. // Kommentare sind zugelassen. $filter = new Zend_Fi1ter_StripTags(array('img','iframe’), array('src'). true); echo $fi1 ter->fi1 ter($html); /* Ausgabe: <!-- body --> Hier ist der Body <img src='bild.jpg’ /> <iframe src='daten.php’> </i frame> // Die Tags img und iframe werden zugelassen. // iframe darf die Attribute width und height haben. // Alle Tags dürfen über das Attribut src verfügen. // Kommentare sind nicht zugelassen $fiIter = new Zend_Fi1ter_StripTags(array('img’=>array(), 'i frame'=> array('width' , 'height')) array('src'). false): echo $fi1 ter->fi1 ter($html); /* Ausgabe: Hier ist der Body <img src='bild.jpg' /> <iframe src='daten.php’ width=’50%’ height=’50%'> </i frame> */ Listing 4.13 Entfernen von Tags 173
e-bol.net 4 | Infrastruktur-Klassen Möchten Sie diese Werte nicht direkt über den Konstruktor setzen oder später wieder verändern, so können Sie die Tags über die Methode setTagsAllowed() und die Attribute über setAttributesAll owed() festlegen. Möchten Sie die Werte wieder auslesen, so stehen entsprechende Methoden zur Verfügung, deren Namen nicht mit set, sondern mit get beginnen. 4.3.13 Nutzung eigener Filter Die bereitgestellten Filter sind zwar schon sehr hilfreich und leistungsfähig, aber in vielen Fällen sicher nicht ausreichend. Benötigen Sie zum Beispiel einen Filter, der nur IP-Adressen akzeptiert, so müssten Sie diesen selbst implementieren. Natürlich könnten Sie auch einfach eine normale Funktion nutzen. Erstellen Sie aber einen Filter, können Sie diesen beispielsweise auch in einer Filter-Chain ver- wenden. Um einen eigenen Filter zu implementieren, benötigen Sie eine Klasse, welche das Interface Zend_Filter_Interface implementiert. Somit muss Ihre Klasse auch die Methode f i l ter() implementieren, was aber auch die einzige Anforde- rung ist. filterO muss einen Parameter, den zu filternden Wert, akzeptieren und den gefilterten Wert zurückgeben. In dem folgenden Beispiel sehen Sie, wie ein solcher Filter implementiert wird. Hier wurde ein Filter umgesetzt, der dafür sorgt, dass Jahreszahlen immer vierstellig sind, wobei Zahlen, die weniger als vier Stellen haben, entsprechend ergänzt werden. Bei Zahlen mit mehr als vier Stellen werden nur die letzten vier Stellen übernommen: requi re_once 'Zend/Filter/Interface.php’; dass JahresFilter implements Zend_Filter_Interface { private $_offset: public function ___construct($offset = 2000) { $this->_offset = $offset; public function filter($string) { $jahr = (int) substr($string,strien($string)-4); if (4 > strien($jahr)) $jahr+=$this->_offset; l return $jahr; 174
e-bol.net Formularverarbeitung mit Zend_Filter_lnput | 4-4 } $filter = new JahresFi1ter(); echo $fi1 ter->fi1 ter(7); // 2007 echo $fi1 ter->fi1 ter(1701); // 1701 echo $fi1ter->fi1 tert 12007); // 2007 Listing 4.14 Implementation eines eigenen Filters 4.4 Formularverarbeitung mit Zend_Filter_lnput Würde man dem Namen nach gehen, so könnte man vermuten, dass Zend_ Fi l ter_Input lediglich ein weiterer Filter ist. Aber Zend_Fi l ter_Input ist mehr und wurde daher auch in ein eigenes Kapitel ausgelagert. Die Möglichkeiten der anderen Filter-Klassen beschränkten sich darauf, Daten zu normalisieren oder zu extrahieren. Allerdings ist keine Möglichkeit der Validie- rung vorgesehen. Wollten Sie nun beispielsweise überprüfen, ob eine E-Mail- Adresse gültig ist, müssten Sie wieder auf Zend_Validate zurückgreifen. Sie müssten also unter Umständen mehrere Filter und Validatoren nacheinander anwenden. Das ist recht umständlich und verwirrend und führt außerdem schnell dazu, dass man etwas übersieht. Zend_Fi l ter_Input gibt Ihnen die Mög- lichkeit, Filter und Validatoren miteinander zu kombinieren. Auf dieser Basis können Sie schnell und einfach Werte aus Formularfeldern übernehmen und prüfen. Zend_Fi l ter_Input ist darauf spezialisiert, Werte zu validieren, zu normalisieren und zu escapen. Das kann Ihnen erhebliche Arbeit abnehmen und Ihren Quell- code deutlich aufräumen. Besonders praktisch ist, dass man ganze Arrays wie zum Beispiel $_POST auf einmal filtern kann. Die grundsätzliche Idee ist einfach. Sie übergeben dem Konstruktor ein Array mit Filtern, eines mit Validatoren sowie ein Array mit den Daten. Um die einzelnen Filter und Validatoren den Formular- bzw. Array-Feldern zuordnen zu können, werden die Array-Schlüssel bzw. die Feldnamen als Schlüssel genutzt. Als Werte können Sie dann die Namen der Klassen nutzen. Oder Sie übergeben direkt ein Objekt der jeweiligen Filter- oder Validator-Klasse. Die Nutzung von Objekten dürfte gerade bei umfangreichen Formularprüfungen performanter sein, da Sie die Objekte mehrfach nutzen können. Vor allem haben Sie bei der Nutzung von Objekten den Vorteil, dass Sie die Objekte auch entsprechend konfigurieren kön- nen. Allerdings müssen Sie dann die benötigen Klassen selbst inkludieren. 175
e-bol.net 4 | Infrastruktur-Klassen Wollen Sie das Array-Element nachname mithilfe von StripTags filtern, könnten Sie das Array so aufbauen: Sfilters = array ( 'nachname' => 'StripTags' ); Oder so: Sstriptags = new Zend_Fi1ter_StripTags(): Sfilters = array ( 'nachname' => Sstriptags ); In einigen Fällen kann es vorkommen, dass Sie mehrere Filter oder Validatoren benötigen. In diesen Fällen übergeben Sie diese als Array an den Schlüssel: Sfilters = array ( 'nachname' => array ('StripTags', 'StringTrim') ); Auch hier wäre natürlich die Nutzung von Objekten möglich gewesen. Wollen Sie, dass eine der Regeln auf alle Felder angewandt wird, so können Sie den Filter oder Validator auch der Wildcard * zuweisen. Der dritte Parameter, der dem Konstruktor von Zend_Fi1ter_Input übergeben wird, ist das Array mit den Daten. Sobald das Objekt initialisiert ist, können Sie mit der Methode i sVal i d() prüfen, ob die übergebenen Daten den Vorgaben der Validatoren entsprechen. Die Methode liefert einen booleschen Wert zurück und kann somit direkt in einer i f- Abfrage genutzt werden. Übergeben Sie isValidO keinen Parameter, werden alle Werte geprüft. Optional können Sie auch den Namen eines Feldes überge- ben, um nur dieses eine Feld zu validieren. Das folgende Listing veranschaulicht die Verwendung: requi re_once 'Zend/Fi 1 ter/Input.php'; // Fi 1terklasssen inkludieren require_once 'Zend/Fi1ter/StringTrim.php'; require_once 'Zend/Filter/StripTags.php’; // Validator-Klassen inkludieren require_once 'Zend/Validate/Int.php’; 176
e-bol.net Formularverarbeitung mit Zend_Filter_lnput | 4-4 // Filterobjekte ableiten Sstringtrim = new Zend_Filter_StringTrim(); Sstriptags = new Zend_Fi1ter_StripTags(); // Filter-Array aufbauen $filter = array ( 'eins' => array($stringtrim, $striptags). 'zwei’ => array ($stringtrim, Sstriptags) ); // Val idator-Objekt ableiten $int = new Zend_Validate_Int(); // Validator-Array aufbauen $validatoren = array ( 'eins' => $int, 'zwei' => $int ): // Array mit Daten erstellen Sdaten = array ('eins' => '42', 'zwei' => 'Zweiundvierzig'); // Fi 1ter_Input-Objekt ableiten Sinput = new Zend_Fi1ter_Input($fi1 ter, $validatoren, Sdaten); // Sind die Daten gültig? if (true — $input->isValid()) { echo "Daten waren gültig"; } el se { $fehler = $input->getMessages(); echo "Daten waren nicht gültig"; echo "<br>Fehler: " .$fehler['zwei' HO]; echo "<br>Wert von eins: ".$input->getEscaped(’eins '); echo "<br>Wert von zwei: ".$input->getEscaped(’zwei'); I Listing 4.15 Nutzung von Zend_Filter_lnput 177
e-bol.net 4 | Infrastruktur-Klassen Da in diesem Fall die Daten nicht gültig sein konnten (der String zwei und vier zig ist kein Integer-Wert), liefert i sVal id() den Wert fal se zurück. Daher habe ich nur den el se-Teil der i f-Abfrage ausgeführt. Die Fehler, die von den Validatoren gemeldet wurden, werden im Objekt abge- legt und können mit der Methode getMessagesl) ausgelesen werden. Überge- ben Sie die Validatoren als Objekte, sollten Sie die Fehlermeldungen natürlich sprachlich anpassen. Nutzen Sie nur die Namen der Validatoren, können Sie die Meldungen auch anpassen. Entnehmen Sie dies bitte der Dokumentation. Die Methode liefert ein mehrdimensionales Array zurück, bei dem in der ersten Ebene die Namen der geprüften Felder als Schlüssel genutzt werden. Jeder dieser Schlüssel verweist wieder auf ein indiziertes Array, welches die Fehlermeldun- gen enthält. In diesem Beispiel ist die vorhandene Fehlermeldung also im Ele- ment [' zwei' ] [ ’ 0' ] enthalten. Die Methode getEscaped() liest den Wert des jeweiligen Feldes aus dem Objekt aus. Hierbei wird der Wert an den Escape-Filter übergeben. Dabei handelt es sich standardmäßig um Zend_Fi 1 ter_Html Enti ties. Damit ist ein recht hohes Maß an Sicherheit gewährleistet, falls die Daten danach wieder ausgegeben werden sollen. Bevorzugen Sie allerdings die »Rohdaten«, so sollten Sie die Methode getUnescaped() verwenden. In dem Fall ist der Begriff »Rohdaten« allerdings nicht ganz exakt, da die Daten gefiltert und validiert, aber nicht escapet wurden. Die Ausgabe im Browser sehen Sie in Abbildung 4.2. O O O http://127.0.0.1/~cars...zf-buch/Filter/l l.php CD fi- Google Daten waren nicht gültig Fehler 'Zweiundvierzig' does not appear to bc an integer Wert von eins: 42 Wen von zwei: Abbildung 4.2 Ausgabe von Zend_Filter_lnput Wie Sie sehen, werden Werte, die nicht validiert werden konnten, vom System nicht wieder bereitgestellt. So weit, so gut. Was aber ist, wenn Sie ein komplettes Formular validieren wol- len? Grundsätzlich macht das natürlich keinen Unterschied zu dem vorangegan- genen Beispiel. Bei genauerer Betrachtung ergeben sich allerdings einige kleinere Probleme. Das erste Problem ist, dass Zend_Fi 1 ter_Enti ties- sofern möglich - UTF-8-Daten akzeptieren sollte, um sicherzustellen, dass es keine Probleme mit 178
e-bol.net Formularverarbeitung mit Zend_Filter_lnput | 4-4 Sonderzeichen gibt. Dazu muss das entsprechende Objekt aber manuell abgelei- tet werden. Sie können es dann mit der Methode setDefaul t Escape Fi 1 ter () als Escape-Filter anmelden, um den normalen Escape-Filter zu überschreiben. Das zweite Problem betrifft die Pflichtfelder. Standardmäßig ist jedes Feld, das der Klasse bekannt ist, ein Pflichtfeld. Sobald Sie einen Filter für ein Feld ange- meldet haben, ist es somit ein Pflichtfeld. Um das zu vermeiden, haben Sie die Möglichkeit, einen sogenannten Meta-Befehl zu nutzen. Dabei handelt es sich um einen Schlüssel, den Sie mit im Array der Validatoren angeben können. Bele- gen Sie den Schlüssel Zend_Fi 1 ter_Input: :ALLOW_EMPTY mit dem Wert true, darf das Feld leer bleiben. Was aber, wenn ein Pflichtfeld nicht mit einem Wert belegt war? Dann erhalten Sie eine entsprechende Meldung, sobald Sie die Methode getMessages!) aufrufen. Standardmäßig lautet die Meldung You must give a non-empty value for field '%fi eld%', wobei %field% durch den Namen des Feldes ersetzt wird. Diese Meldung können Sie dadurch ersetzen, dass Sie dem Konstruktor noch ein viertes Array als Parameter übergeben. In diesem kön- nen Sie den Schlüssel Zend_Fi 1 ter_Input::NOT_EMPTY_MESSAGE mit einer eige- nen Meldung belegen. Diese Option können Sie auch später noch mit der Methode setOptions() festlegen. In dem folgenden Listing wird ein Formular mit drei Feldern ausgegeben. Der Vorname ist ein optionales Feld. Nachname und E-Mail-Adresse sind Pflichtfel- der, wobei die E-Mail-Adresse auch auf Validität hin geprüft wird: header('Content-Type: text/html; charset=utf-8'): requi re_once 'Zend/Fi 1 ter/Input.php'; // Fi 1terklasssen inkludieren require_once 'Zend/Fi1ter/StringTrim.php'; require_once 'Zend/Filter/StripTags.php’; requi re_once 'Zend/Fi1 ter/HtmlEntities.php’; // Validator-Klassen inkludieren require_once 'Zend/Validate/EmailAddress.php'; function formular_ausgeben!$i nput) { // Werte auslesen $v_vorname = $input->getEscaped('Vorname'): $v_nachname = $input->getEscaped(’Nachname'): $v_email = $input->getEscaped(’E-Mai 1'): // Eventuelle Fehlermeldungen auslesen 179
e-bol.net 4 | Infrastruktur-Klassen $fehler = $input->getMessages(); echo "<form method=’post'>"; echo "Vorname: <input type='text' name=’Vorname’ value='$v_vorname’><br>”; echo "Nachname: <input type='text' name=’Nachname' value='$v_nachname'><br>"; if (true —== isset ($fehler['Nachname'])) { echo implode('<br>',$fehler['Nachname']).'<br>'; echo "E-Mai 1-Adresse: <input type=’text' name='E-Mail' value='$v_emai1'><br>"; if (true —== isset ($fehler['E-Mai 1'])) { echo implode('<br>',$fehler['E-Mail’]).'<br>'; echo "<input type=’submit' value=’Abschicken'><br>"; echo "</form>"; // Loaklisierungseinstel1ungen setzen setl ocale(LC_ALL,'de_DE'); // Filterobjekte ableiten Sstringtrim = new Zend_Filter_StringTrim(); Sstriptags = new Zend_Fi1ter_StripTags(): // Validator-Objekt ableiten Semail = new Zend_Validate_Emai1Address(): $emai1->setMessage('Die E-Mai 1-Adresse ist nicht g&uuml;ltig*, Zend_Validate_Emai1 Address::INVALID); // Hier noch weitere Meldungen setzen... // Escape-Fi 1 ter ableiten SescapeFi1 ter = new Zend_Fi1ter_HtmlEntities(ENT_COMPAT, ’UTF-81): // Filter-Array aufbauen $filter = array ( 'Vorname' => array($stringtrim, $striptags). 'Nachname' => array($stringtrim, 180
e-bol.net Formularverarbeitung mit Zend_Filter_lnput | 4-4 $striptags). ’E-Mai 1 * => array ($stringtrim, $stri ptags) // Val idator-Array aufbauen $va1idatoren = array ( 'Vorname' => array( Zend_Fi1ter_Input::ALLOW_EMPTY => true), 'Nachname' => array( Zend_Fi1ter_Input::BREAK_CHAIN => false), 'E-Mail’=> $emai1 ) ; // Meldung für Pflichtfelder Soptionen = array ( Zend_Fi1ter.Input::NOT_EMPTY_MESSAGE => 'Das Feld "%field%" darf nicht leer sein' // Fi 1ter_Input-Objekt ableiten Sinput = new Zend_Fi1ter_Input($fi1 ter, $validatoren, $_POST, $optionen); // Escape-Filter setzen $input->setDefaultEscapeFilter($escapeFilter); // Wurden Daten übergeben // Und waren die Daten gültig? if (0 !== count($_POST) && true === $input->isVa1id()) echo "Sie gaben folgende Daten ein:"; echo "<br>Vorname: ".$input->getEscaped('Vorname'): echo "<br>Nachname: $input->getEscaped('Nachname'); echo "<br>E-Mail: $input->getEscaped('E-Mai 1’); I el se { formular_ausgeben(Sinput); } Listing 4.16 Validieren eines Formulars
e-bol.net 4 | Infrastruktur-Klassen Die meisten Elemente sind Ihnen ja inzwischen bekannt, daher nur ein paar kleine Ergänzungen. Bei dem E-Mail-Validator habe ich aus Platzgründen nur eine Fehlermeldung eingedeutscht. Die anderen sollten natürlich auch übersetzt werden. Bei dem Array mit den Validatoren finden Sie noch einen weiteren Meta-Befehl. Mit Zend_F11ter_Input: :BREAK_CHAIN -> false wird sichergestellt, dass die Validatoren auch dann weiter durchlaufen, wenn einer fehlschlägt, also wenn in diesem Fall das Feld Nachname leer wäre. Ein kleiner Tipp am Rande: Sollten Sie für ein Feld keine Validatoren benötigen, so muss das entsprechende Feld trotz- dem mit in dem Validator-Array genannt werden. Andernfalls geht der Wert des Feldes verloren. Belegen Sie das Feld in diesem Fall einfach mit einem leeren Array, also mit array(). Die i f-Abfrage, die prüft, ob die Daten korrekt sind, muss beim ersten Aufruf feststellen können, ob überhaupt Daten aus dem Formular übergeben wurden. Das geschieht dadurch, dass die Anzahl der Werte in $_POST ermittelt wird. isValich ) liefert, auch wenn keine Daten übergeben wurden, nämlich ebenfalls true zurück. Man hätte an dieser Stelle vorher auch die Methode processt) auf- rufen können. Sie wirft eine Exception, wenn erwartete Felder nicht vorhanden sind. Das Formular hätte daher im Rahmen des Exception Handlings ausgegeben werden lassen können. Das Formular wird von der Funktion formiil ar_ausgeben() ausgegeben. Dabei fallt auf, dass die Namen der Formularfelder ungewöhnlich sind. Üblicherweise würde man ein Formularfeld eher email nennen und nicht E-Mail. Diese Namensgebung ist aber bewusst so gewählt, weil diese Namen auch in der Feh- lermeldung genutzt werden, die ausgegeben wird, falls ein Pflichtfeld leer ist. In Abbildung 4.3 sehen Sie das Formular, wie es dargestellt wird, wenn kein Nachname eingegeben wurde und die E-Mail-Adresse ungültig war. n http://127.0.0...ilter/zwei.php CD Vorname: Car>ttn | Nachname: Das Feld "Nachname" darf nicht leer sein E-Mail-Adrcssc: Die E-Mail-Adresse ist nicht gültig Abschicken ' Abbildung 4.3 Validiertes Formular 182
e-bol.net Formularverarbeitung mit Zend_Filter_lnput | 4-4 Mit der oben vorgestellten Lösung können Sie schon einen Großteil der üblichen Anforderungen abdecken. Allerdings gibt es noch einige andere Funktionalitä- ten, die in dem Paket umgesetzt sind. Die Methode getMessages() liest alle Mel- dungen aus, die bei der Prüfung der Werte generiert wurden. Möchten Sie nur bestimmte Meldungen erhalten, so stehen die Methoden get Inval id(), getMi s- si ng() und getUnknown() zur Verfügung. Die erste liest die Namen und Meldun- gen derjenigen Felder zurück, die nicht validiert werden konnten. Das Format entspricht dem, das bereits bei getMessagesI) beschrieben wurde. getMis- si ng() liefert Ihnen Informationen zu Feldern, die erwartet wurden, aber nicht im Daten-Array enthalten waren. Grundsätzlich gelten alle Felder als optional, werden also nicht unbedingt erwartet. »Nicht erwartet« bedeutet, dass das Feld gar nicht mit den Formulardaten verschickt wurde, beziehungsweise im Daten- Array kein Schlüssel mit diesem Namen vorhanden ist. Um festzulegen, dass ein Feld im Ergebnis enthalten sein muss, nutzen Sie den Meta-Befehl ist Zend_ Fi 1ter_Input::PRESENCE. Mit Zend_Fi1ter_Input::PRESENCE=>Zend_Fi1ter_ Input::PRESENCE_REQUIRED legen Sie fest, dass das Ergebnis enthalten sein muss. Und mit Zend_Fi 1 ter_Input:: PRESENCE=>Zend_Fi 1 ter_Input:: PRESENCE_ OPTIONAL definieren Sie, dass das Feld optional ist. Welche Fehlermeldung genutzt werden soll, können Sie im Optionen-Array (vierter Parameter des Kon- truktors) über den Schlüssel Zend_Fi 1ter_Input: :MISSING_MESSAGE definieren. Abschließend gibt es noch die Methode getUnknown(), welche Ihnen die Namen von Feldern zurückgibt, die im Daten-Array enthalten waren, für die aber keine Regeln zur Verarbeitung vorgesehen waren. Bei der Nutzung von getMessages() haben Sie schon gesehen, dass Sie diese Methoden jederzeit aufrufen können, selbst wenn noch keine Meldungen vorhanden sind. In diesem Fall erhalten Sie ein leeres Array zurück. Wollen Sie aber vorher testen, ob Meldungen vorliegen, können Sie die Methoden haslnvalid(), hasMissing() und hasUnknown() abfragen, die jeweils einen booleschen Wert zurückgeben. In einigen Fällen kann es passieren, dass Sie in einem Formular ein Array benö- tigen. Wollen Sie beispielsweise ein Datum abfragen, besteht dieses unter Umständen aus drei Textfeldern, die logisch zusammengehören. Die Felder im HTML-Formular könnten so aussehen: Cinput type="text" name=''dat[tag]" size="2"> <input type="text" name=''dat[inonat]" size="2"> Cinput type="text" name=''dat[jahr]" size="4"> Wollen Sie auf alle Felder die selben Filter oder Validatoren anwenden, ist dies kein Problem. Nutzen Sie einfach dat als Namen. Das Paket geht dann durch alle untergeordneten Array-Felder und verarbeitet sie. 183
e-bol.net 4 | Infrastruktur-Klassen Wenn Sie unterschiedliche Filter nutzen wollen, sieht die Sache ein wenig kom- plizierter aus. In diesem Fall können Sie folgenden Aufbau nutzen: $val1datoren = array ( ’dat' => array( array( ' Int', Zend_Fi1ter_Input::FIELDS => 'tag’), array( ' Int', Zend_Fi1ter_Input::FIELDS => ’monat'), array( ' Int' , Zend_Fi1ter_Input::FIELDS => 'jähr') )); Der Meta-Befehl Zend_Filter_Input:: FI ELDS bietet Ihnen also die Möglichkeit, auf einzelne Felder zuzugreifen. Allerdings ist die Nutzung von Arrays momen- tan leider noch fehlerbehaftet. Daher würde ich Ihnen zum jetzigen Zeitpunkt (Version 1.0.3) noch davon abraten, Arrays zu nutzen. Sollten Sie eine neuere Version nutzen, so prüfen Sie bitte genau, ob die Fehlermeldungen korrekt zuge- ordnet werden und die Nutzung von mehreren Validatoren möglich ist. Derzeit kann nur ein Validator genutzt werden. Auch bei dieser Klasse gilt, dass es noch verschiedene weitere Vorgehensweisen gibt, um die hier beschriebenen Funktionalitäten zu nutzen. Sie finden sie in der Dokumentation zur Klasse. 4.5 Schreiben von Logs mit Zend_Log Wenn Sie schon größere Applikationen entwickelt haben, werden Sie das Gefühl kennen, dass Sie gerne wüssten, was im Inneren der Applikation abläuft. Ab einer bestimmten Größe sind Applikationen schwer zu überwachen. Zwar kann ein Error-Handler Alarm schlagen, sobald ein Fehler auftritt, aber oft kann man dann nicht mehr erkennen, was zum Problem geführt hat. Ein Error-Handler kann Ihnen ebenfalls nicht dabei helfen, Performance-Engpässe aufzuspüren. Um solche Probleme in den Griff zu bekommen oder Sie beim Debuggen Ihrer Applikation zu unterstützen, ist es sehr hilfreich, ein Log zu schreiben. Genau dies leistet Zend_Log. Die Nachrichten, die das System speichert, können unter- schiedliche Schweregrade ausdrücken. Das heißt, Sie können beispielsweise zwi- schen einfachen Informationen, Warnungen, Fehlern und schweren Fehlern unterscheiden. Zend_Log besitzt dabei ein Feature, das wirklich sehr praktisch ist. 184
e-bol.net Schreiben von Logs mit Zend_Log | 4-5 Und zwar können Sie Nachrichtenlevel filtern. Sie können während der Debug- Phase so mehr Informationen speichern und später umschalten und nur noch echte Fehler speichern, wenn das System in den Echtbetrieb geht. Ein wirklich hilfreiches Feature, wie ich finde. Ein Log-Eintrag der Klasse besteht standardmäßig aus der eigentlichen Meldung, einem Timestamp und der Priorität, die als Zahl und als Text eingefügt wird. Der Timestamp liegt dabei im ISO 8601-Format vor, das wie folgt aufgebaut ist: YYYY - MM- DDTHH: MM: SS+HH: MM. Bei dem Teil vor dem T handelt es sich um das Datum. Das T ist konstant. Danach folgt die Uhrzeit. An diese schließt sich (getrennt von einem + oder einem -) die Abweichung der aktuellen Zeitzone von Greenwich Mean Time an. Die Uhrzeit wird dabei in der für die Zeitzone korrekten Uhrzeit ausgegeben. Bei der Verarbeitung eines Timestamps kann Zend_Date sehr nütz- lich sein. Standardmäßig kennt das Paket acht Prioritäten, die auch als Log-Level bezeich- net werden. Sie finden diese in Tabelle 4.2. Name Zahl Bedeutung Kurzname Emergency 0 Schwerwiegender kritischer Fehler. System steht kom- plett. EMERG Alert 1 Kritischer Zustand, der sofortiges Eingreifen erfordert ALERT Critical 2 Kritischer Zustand des Systems CRIT Error 3 Fehler bei der Programmausführung, der das System nicht gefährdet ERR Warning 4 Eine Warnung ist ein unerwarteter Zustand, der aber (noch) kein Fehler ist. Beispiel: Die Festplatte des Servers ist zu 90% belegt. WARN Notice 5 Normale Ausführung des Codes, wobei es zu Auffälligkei- ten (z. B. verlängerte Laufzeit) kam. NOTICE Informational 6 Rein informative Nachricht Beispiel: Anzahl der Datensätze, die bei einem Update verändert wurden INFO Debug 7 Debug-Information, die beim Debugging helfen soll DEBUG Tabelle 4.2 Verschiedene Log-Level Möchten Sie ein Log schreiben, so benötigen Sie dazu mindestens zwei Kompo- nenten. Das ist zum einen der Logger selbst, der für die Verwaltung der Nach- richten zuständig ist. Zum anderen ist dies mindestens ein sogenannter Writer. Dieser kümmert sich darum, die Daten in eine Datei zu schreiben oder in einem anderen Speichermedium abzulegen. 185
e-bol.net 4 | Infrastruktur-Klassen Optional können Sie die Nachrichten noch formatieren oder filtern. Dazu erfah- ren Sie später mehr. Ein Log-Objekt ohne Writer-Objekt ist nicht besonders sinnvoll, da die Log-Ein- träge sonst nirgendwo gespeichert werden könnten. Daher können Sie das Wri- ter-Objekt direkt an den Konstruktor der Klasse Zend_Log übergeben. Alternativ können Sie ihn auch später mithilfe der Methode addWri ter() hinzufügen. Das wäre auch die Vorgehensweise, wenn Sie mehr als einen Writer nutzen wollen. Dies kann beispielsweise dann hilfreich sein, wenn Sie die Daten zwar in einer Datenbank ablegen möchten, sie aus Sicherheitsgründen aber auf jeden Fall auch noch auf die Festplatte des Servers schreiben wollen. requi re_once('Zend/Log.php'); requi re_once('Zend/Log/Weiter/Stream.php'); try { $writer = new Zend_Log_Writer_Stream(1Zvar/1og/my_log'); $logger = new Zend_Log($writer); $logger->info('Hallo Welt } catch (Zend_Log_Exception $e) { die ($e->getMessage()); } Listing 4.17 Nutzung des Loggers In Listing 4.17 sehen Sie ein kleines Beispiel. Hier wird zunächst der Writer abge- leitet und dann an den Logger übergeben. Wie Sie sehen, müssen die Klassen- Dateien des Writers und des Loggers inkludiert werden. Der hierbei generierte Log-Datei-Eintrag lautet: 2007-09-13T20:23:02+02:00 INFO (6): Hallo Welt ;-) Der Stream-Writer ist ein recht flexibler Writer. Sie können ihm direkt den Namen und den Pfad der Log-Datei übergeben, was sicherlich am einfachsten ist. Alternativ können Sie auch einen bereits geöffneten Stream übergeben oder einen String, der einen Stream3 definiert. In allen Fällen muss es natürlich möglich sein, dass der Logger in den Stream schreiben kann, wobei bestehende Log-Einträge nicht überschrieben werden. Eine andere Variante, um Daten zu speichern, besteht darin, die Daten in eine Datenbank zu schreiben. In diesem Fall wäre der Writer Zend_Log_Writer_Db 3 Weitere Informationen zu Streams finden Sie unter http://www.php.net/streams. 186
e-bol.net Schreiben von Logs mit Zend_Log | 4-5 dafür zuständig. In dem folgenden kleinen Beispiel wurde die folgende MySQL- Tabelle zugrunde gelegt: Create Table: CREATE TABLE 'log_data' ( 'id' int(ll) NOT NULL AUT0_INCREMENT. 'meldung' text NOT NULL, 'prio' int(ll) DEFAULT NULL, 'prio_name' varchar(6) DEFAULT NULL. 'Zeitpunkt' datetime DEFAULT NULL, PRIMARY KEY ('id' ) ) ENGINE=MyISAM DEFAULT CHARSET=1atinl Listing 4.18 Tabelle zum Speichern von Log-Einträgen Der Datenbank-Logger benötigt ein Zend_Db-Objekt, um die Verbindung zur Datenbank herzustellen. Das Datenbankobjekt wird dann direkt an den Kon- struktor des Writers übergeben. Als zweiten Parameter bekommt der Writer den Namen der Tabelle als String übergeben. Der dritte Parameter ist ein Array, das dazu dient, die Log-Informationen den Datenbankspalten zuzuordnen. Standard- mäßig werden dabei intern in der Log-Klasse die folgenden Bezeichner verwen- det: Bezeichner Inhalt priority Priorität als Zahl pri ori tyName Priorität als Text message Text des Log-Eintrags timestamp Zeitpunkt des Ereignisses Tabelle 4.3 Schlüssel für das AAapping der Datenbankspalten In dem Array wird der Name der Datenbankspalte als Schlüssel genutzt und der dazugehörige Bezeichner aus Tabelle 4.3 als Wert. requi re_once(’Zend/Db.php’); requi re_once(’Zend/Log/Writer/Db.php’); requi re_once(’Zend/Log.php’); $params = array ('host' => *127.0.0.1', ’username' => 'root’, ’password' => ’’. ’dbname’ => 'test’): $db = Zend_Db::factory('PDO_MYSQL', $params): // Mapping der Spalten auf die Bezeichner Smapping = array('prio' => ’priority', ’prio_name' => ’priorityName', 187
e-bol.net 4 | Infrastruktur-Klassen ’meldung’ => ’message', ’ zei tpunkt'=>'timestamp' ) ; try { // Writer-Objekt ableiten $writer = new Zend_Log_Writer_Db($db, 'log_data', $mapping); // Neues Log-Objekt ableiten $logger = new Zend_Log($writer); // Eintrag in die Datenbank schreiben $logger->info('Dies ist eine schicke Info'); } catch (Zend_Log_Exception $e) { die ($e->getMessage()); } Listing 4.19 Generieren eines Log-Eintrags in einer Datenbank Ein Eintrag in der Datenbank sieht dann beispielsweise so aus: mysql> select * from log_data \G k "k "k k "k k k “k "k "k k k -k k k -k k k k k k k k k k k k J FOW "kkkkkkkkkkkkkkkkkkkkkkkkkkk id: 1 meldung: Dies ist eine schicke Info prio: 6 prio_name: INFO Zeitpunkt: 2007-0913 19:59:21 1 row in set (0.00 sec) Die Zeitzoneninformation geht beim Schreiben in eine normale Datetime-Spalte verloren, was üblicherweise aber nicht dramatisch ist. Ob Sie die Daten nun in einen Stream, in eine Datei oder eine Datenbank schrei- ben, ist eine Geschmacksfrage. Ich empfehle Ihnen, mit einer Datei zu arbeiten. Eine Datenbank zu nutzen, kann unter Umständen - gerade dann, wenn Netz- werk- oder Datenbankprobleme auftreten -, schwierig sein. Während der Entwicklung kann es durchaus hilfreich sein, mit einem Mock-Wri- ter4 zu arbeiten oder die Nachrichten direkt zu verwerfen. Um die Nachrichten direkt zu verwerfen, können Sie Zend_Log_Wri ter_Nul 1 nutzen. Nutzen Sie die- sen Writer, so können Sie zwar Einträge generieren, diese erzeugen aber kein Resultat. 4 Mock: engL: Fälschung, Attrappe 188
e-bol.net Schreiben von Logs mit Zend_Log | 4-5 Ein Mock-Writer, der von der Klasse Zend_Log_Wri ter_Mock abgeleitet wird, legt die Nachrichten direkt in dem Objekt ab. Das heißt, Sie können dann beispiels- weise einfach mit var_dump() ausgeben lassen, welche Log-Informationen bis dato aufgelaufen sind. Damit werden die Log-Datei bzw. die Datenbank nicht unnötig belastet. Bisher haben Sie nur die Methode i nfo() kennengelernt, mit der Log-Einträge generiert wurden. Auch für die anderen Log-Level ist jeweils eine Methode defi- niert, die Sie nutzen können. Die Namen der Methoden entsprechen jeweils den Kurznamen der Events, die Sie in Tabelle 4.2 kennengelernt haben: info(), debug(), notice(), warnt), errort), crit() und emerg(). Alle Methoden erhal- ten jeweils den Text der Meldung als Parameter übergeben. Alternativ können Sie auch die Methode logt) benutzen. Diese erhält als ersten Wert die Log-Meldung und als zweiten die Priorität des Eintrags als Zahl überge- ben. Ich meine, dass die vorhandenen Log-Level üblicherweise ausreichend sein soll- ten. Haben Sie zu viele Level, so ist die feine Granularität in den meisten Fällen eher verwirrend und nicht wirklich hilfreich. Hier und da kann es aber vorkom- men, dass sich im Laufe der Entwicklung noch ein spezielles Problem ergibt, das einen neuen Level erfordert. Das kann zum Beispiel dann passieren, wenn ein spezieller Level vorhanden sein sollte, bei dem nur der Datenbankadministrator benachrichtigt werden soll. In solch einem Fall können Sie mithilfe von $logger->addPriority(’DB_ADMIN', 8); einen neuen Level, in diesem Beispiel 8, hinzufügen. Hier ist der Kurzname des Levels DB_ADMI N. Die Methode, über die der Log-Eintrag erstellt wird, wird dyna- misch generiert und hat ebenfalls den Namen db_admin(). Die Tatsache, dass die Methoden auf diesem Weg dynamisch generiert werden, hat übrigens auch eine unter Umständen verwirrende Nebenerscheinung. Versu- chen Sie, aus einem Logger-Objekt heraus eine Methode aufzurufen, die es nicht gibt, so erhalten Sie die Fehlermeldung, dass der Log-Level nicht korrekt ist. Hin- tergrund ist, dass das Paket mithilfe der magischen Methode_cal 1 () den Ein- trag in die Log-Datei vornimmt. Diese bekommt den Namen der Methode über- geben, die Sie aufgerufen haben, und interpretiert ihn als Log-Level. 189
e-bol.net 4 | Infrastruktur-Klassen 4.5.1 Log-Einträge filtern Wie schon erwähnt, gibt es in Zend_Log die interessante Möglichkeit, Log-Nach- richten zu filtern und nur bestimmte Einträge zuzulassen. Ein Filter wird immer von seiner eigenen Klasse abgeleitet und dann an den Logger übergeben, wozu der Logger die Methode addFilterO vorsieht. Sie bekommt das Filter-Objekt direkt als Parameter übergeben. Das Paket kennt dazu drei Arten von Filter-Klas- sen. Der einfachste Filter ist Zend_Log_Fi l ter_Suppress. Mit ihm können Sie alle Einträge unterdrücken oder alle komplett akzeptieren. Das kann dann hilfreich sein, wenn Sie eine Änderung an einem Live-System vornehmen und nicht riskie- ren möchten, eine zu große Anzahl von Log-Einträgen zu generieren. Praktisch daran ist, dass der Filter entweder alle Nachrichten passieren lässt oder alle Ein- träge ignoriert. Das Verhalten wird mithilfe der Methode suppress() gesteuert, die einen booleschen Wert übergeben bekommt. Der Wert true sorgt dabei dafür, dass alle Nachrichten ignoriert werden. requi re_once('Zend/Log.php'); requi re_once('Zend/Log/Wri ter/Stream.php'); requi re_once('Zend/Log/Filter/Suppress.php'); try { // In Datei schreiben $writer = new Zend_Log_Writer_Stream('logfile'); // Neuen Logger ableiten $logger = new Zend_Log($writer); // Neuen Suppress-Filter $filter = new Zend_Log_Filter_Suppress(); // Filter an Logger übergeben $logger->addFilter($filter); // Unterdrücken aller Nachrichten einschalten $filter->suppress(true); // Dieser Eintrag wird unterdrückt $logger->info('Diese Meldung kommt nicht an'); } catch (Zend_Log_Exception $e) { die ($e->getMessage()); } Listing 4.20 Filtern von Log-Einträgen 190
e-bol.net Schreiben von Logs mit Zend_Log | 4-5 Der zweite Filter, der üblicherweise sehr praktisch ist, lautet Zend_Log_Fi 1 ter_ Pri ori ty. Hierbei werden nur Einträge übernommen, welche einen bestimmten Log-Level übersteigen. Das heißt, Sie können beispielsweise dafür sorgen, dass nur Einträge mit einem Level übernommen werden, der »wichtiger« ist als INFO. Der Konstruktor bekommt hierbei den Log-Level übergeben, dem ein Eintrag mindestens entsprechen muss, damit er übernommen wird. Um also die INFO- und DEBUG-Nachrichten zu unterdrücken, könnte man dem Kontruktor die Kon- stante Zend_Log: :WARN übergeben. Die Konstanten entsprechen den Kurznamen der Level aus Tabelle 4.2. // Nur Nachrichten deren Level größer oder gleich Warning ist Sfilter = new Zend_Log_Fi1ter_Priority(Zend_Log::WARN); // Filter hinzufügen $logger->addFilter($filter); // Diese Meldung kommt ins Log... $1ogger->warn('Diese Meldung kommt an'); // ...diese nicht $1ogger->info('Diese Meldung kommt nicht an’); Bitte beachten Sie, dass ein Level, den Sie selbst hinzugefügt haben, diesem Filter schnell zum Opfer fallen kann. Er besitzt immer den niedrigsten Level. Der Prio- rity-Filter muss übrigens nicht explizit inkludiert werden. Das macht die Log- Klasse selbst. In so einem Fall kann der letzte Filter Ihnen aber eventuell weiterhelfen. Er kann Nachrichten mithilfe eines regulären Ausdrucks bewerten und in Abhängigkeit davon passieren lassen oder verwerfen. Beim Ableiten des Objekts der Filter- Klasse Zend_Log_Fi 1 ter_Message wird der reguläre Ausdruck direkt an den Kon- struktor übergeben. Sobald Sie den Filter zugewiesen haben, wird die Nachricht jedes Log-Eintrags mit dem regulären Ausdruck verglichen. Nur eine Nachricht, auf die der Ausdruck zutrifft, wird ins Log übernommen. Sie können übrigens auch mehrere Filter kombinieren, indem Sie mehrfach die Methode addFi 1 ter() aufrufen und jeweils einen Filter übergeben. 4.5.2 Logfile-Einträge formatieren Neben der Möglichkeit, Daten zu filtern, können Sie Log-Einträge auch umfor- matieren, was natürlich nur dann sinnvoll ist, wenn Sie eine Log-Datei schreiben. Bei der Nutzung einer Datenbank werden die Daten erst beim Auslesen aus der Datenbank formatiert. 191
e-bol.net 4 | Infrastruktur-Klassen Ein gewöhnlicher »Standardeintrag« in einer Log-Datei ist wie folgt aufgebaut: 2007-09-13T20:23:02+02:00 INFO (6): Hallo Welt ;-) Möchten Sie die Daten aber beispielsweise im CSV-Format oder vielleicht in einem XML-Format erzeugen, so müssen Sie diese umformatieren. Um Daten zu formatieren, benötigen Sie einen Formatter. Zurzeit kennt das Fra- mework zwei Klassen, die diesen Zweck erfüllen: Zend_Log_Formatter_Simple und Zend_Log_Formatter_Xml. Der erste dient dazu, Textdateien zu formatieren. Der zweite unterstützt Sie dabei, Daten im XML-Formt auszugeben. In beiden Fällen muss zuerst ein Objekt der entsprechenden Klasse abgeleitet werden, das dann mithilfe von setFormatterl) an das Writer-Objekt übergeben wird. Der Simple-Formatter bekommt einen Format-String übergeben, welcher defi- niert, wie ein Eintrag aussehen soll. In diesem String können Sie Platzhalter benutzen, um festzulegen, an welcher Stelle welche Information ausgegeben werden soll. Die Namen der Platzhalter entsprechen denen aus Tabelle 4.3, wobei Sie bitte vor und hinter dem Platzhalter ein Prozentzeichen (%) ergänzen. Alle anderen Zeichen, die Sie in dem Format-String nutzen, werden direkt in die Ausgabe übernommen. Somit können Sie also beispielsweise auch ein Semikolon (;) nutzen, falls Sie eine CSV-Datei erzeugen möchten: requi re_once('Zend/Log.php'); requi re_once('Zend/Log/Writer/Stream.php'); requi re_once('Zend/Log/Formatter/Simple.php’); try { // Neuen Writer ableiten $writer = new Zend_Log_Writer_Stream(11ogfi1e'); // Format definieren $format = '%timestamp%;%priorityName%;' .'(%priority%);%message%' . PHP_EOL; // Formatter-Objekt ableiten $formatter = new Zend_Log_Formatter_Simple($format); // Formatter an den Writer übergeben $wri ter->setFormatter(tformatter); $logger = new Zend_Log($writer); $logger->info('Diese Datei ist im CSV-Format'); } catch (Zend_Log_Exception $e) 192
e-bol.net Schreiben von Logs mit Zend_Log | 4-5 die ($e->getMessage()): } Listing 4.21 Schreiben in eine CSV-Datei Das Script aus Listing 4.21 erzeugt Einträge in diesem Format: 2007-09-14T12:11:05+02:00:INFO:(6);Diese Datei ist im CSV-Format Auch die Verwendung des XML-Formatters ist recht unproblematisch. Allerdings sollte an dieser Stelle erwähnt werden, dass der Formatter von sich aus nicht in der Lage ist, valide XML-Dateien zu erzeugen. Das Problem besteht in dem Root- Element, das der Formatter immer vom Ende der Datei entfernen und dann wie- der anfügen müsste. Möglich wäre das natürlich, aber leider auch sehr Perfor- mance intensiv. Daher hat man auf die Nutzung eines Root-Elements verzichtet. Nur jeder Eintrag selbst ist von einem »Root-Element« eingeschlossen. Möchten Sie die Datei also mit DOM oder SimpleXML verarbeiten, müssen Sie beim Aus- lesen manuell ein Root-Element ergänzen. Der XML-Formatter benötigt keine Parameter. Sie können daher einfach ein ent- sprechendes Objekt an den Writer übergeben, und die einzelnen Elemente erhal- ten daraufhin die Bezeichner aus Tabelle 4.3 als Namen zugewiesen. <1ogEntry> <timestamp>2007-09-14T12:23:24+02:00</timestamp> <message>Eintrag im XML-Format</message> <pri ori ty>6</pri ori ty> <pri ori tyName>INFO</pri ori tyName> </logEntry> Allerdings können Sie dem Konstruktor des Formatters auch eigene Vorgaben für die Formatierung der XML-Daten übergeben. Der erste Parameter ist dabei ein String, der den Namen des Root-Elements definiert. Danach folgt ein Mapping- Array. Hierbei wird der Tag-Name, den Sie vergeben wollen, als Schlüssel genutzt. Der Wert stellt dann den internen Namen dar. Mit der Deklaration Smapping = arrayl'prio' => ’priority’, ’prio_name' => ’priorityName', ’meldung’ => ’message', ’zei tpunkt'=>'timestamp’ ): Sformatter = new Zend_Log_Formatter_Xml('EINTRAG', $mapping); $wri ter->setFormatter($ Formatter); 193
e-bol.net 4 | Infrastruktur-Klassen würden also folgende Einträge erzeugt: <EINTRAG> <pri o>6</pri o> <pri o_name>INFO</pri o_name> <meldung>Diese ist im CSV-Form at</meldung> <zei tpunkt>2007-09-14T12:54:01+02:00</zei tpunkt> </EINTRAG> 4.5.3 Eigene Einträge definieren Alle bisher vorgestellten Varianten haben den gleichen Informationsgehalt gespei- chert. Hier und da kann es aber hilfreich sein, mehr Informationen zu speichern. Auch das ist möglich. Und zwar können Sie neue »Items« definieren, also Einträge, die dann mit ins Log geschrieben werden. Ein Item muss immer mit setEvent- Item() beim Logger angemeldet werden. Die Methode bekommt als ersten Para- meter den »internen Namen« übergeben und als zweiten den Wert, der gespei- chert werden soll. Zurzeit ist dieser Wert bei jedem Eintrag konstant; es ist momentan also nicht möglich, eine Funktion anzugeben, die bei jedem Eintrag aufgerufen wird. Dennoch kann die Funktionalität sehr hilfreich sein, wenn Sie zum Beispiel einen Benutzernamen, den Namen des Datenbankservers oder Ähn- liches speichern wollen. Einen neuen Eintrag legen Sie beispielsweise wie folgt an: $logger->setEventItem(1userid' , $_SESSION['userid'1): Wichtig ist aber, dass die Standard-Writer den Eintrag noch nicht speichern, da sie das Item nicht kennen. Bei dem Stream-Writer benutzen Sie bitte einen For- matter, der das neue Item inkludiert. Bei dem Datenbank-Writer müssten Sie das Item mit in das Mapping-Array aufnehmen. 4.6 Konfigurationsverwaltung Zend_Config Sicher kennen Sie das folgende Problem auch. Sie erstellen eine größere Applika- tion, die konfiguriert werden muss. Um zu verhindern, dass Daten im Quelltext geändert werden müssen, lagern Sie die Konfiguration in eine andere Datei aus. Oft findet man hierbei die Vorgehensweise, dass die Werte direkt an PHP-Vari- ablen übergeben werden. Das ist aber häufig nicht gewünscht. In diesem Fall haben Sie die Möglichkeit, mit einer INI-Datei zu arbeiten oder, was sich gerade in komplizierten Fällen anbietet, XML-Code zu nutzen. Um die Konfigurationsdaten auf möglichst einfachem Weg zur Verfügung zu stel- len, bietet sich Zend_Config an. Das Paket unterstützt sowohl die Arbeit mit einem einfachen Array als auch die Nutzung von INI- und XML-Dateien. 194
e-bol.net Konfigurationsverwaltung Zend_Config | 4-6 4.6.1 Nutzung von Konfigurations-Arrays Die Daten, die in einer solchen Konfigurationsdatei enthalten sind, stellt die Klasse dann als Eigenschaften des Objekts, das Sie ableiten, zur Verfügung. Bevor ich mich noch länger in der Theorie verliere, möchte ich Ihnen an dem folgenden kleinen Beispiel zeigen, wie die Nutzung von Zend_Conf i g auf Basis eines Arrays aussieht: require_once 'Zend/Config.php'; // Konfigurationsdaten $conf_daten = array ( 'Server' => '127.0.0.1', ’ user' => 'root', ’password' => '', ’tabelle’ => array ('name' => 'test.userdaten', 'spaltel' => ’benutzer', 'spalte2' => ’passwort' ) ): try { // Ableiten des Objekts und Übergabe der Daten tconfig = new Zend_Config ($conf_daten); //Nutzung von Daten der ersten Ebene $db = mysql_connect($config->server, $config->user, $config->password); // Zugriff auf Daten der zweiten Ebene // $user und $password wurden vorher belegt $sql = 'SELECT * FROM ’.$config->tabel1e->name.' WHERE '. $config->tabel1e->spaltel . "= '$user' AND ". $config->tabel1e->spalte2 . " = ’$password: } catch (Zend_Config_Exception $e) { die ($e->getMessage()): 1 Listing 4.22 Nutzung eines Arrays mit Konfigurationsdaten Wie Sie in diesem Beispiel sehen, können Sie das Array einfach an den Konstruk- tor übergeben, der die Daten dann als Eigenschaften zur Verfügung stellt. In die- 195
e-bol.net 4 | Infrastruktur-Klassen sem Beispiel wird ein zweidimensionales Array genutzt. Sie können allerdings auch mehr Dimensionen nutzen. Beim Verbindungsaufbau zur Datenbank werden die Daten der ersten Hierar- chieebene genutzt, wohingegen beim Zugriff auf die Tabelle die Daten des ver- schachtelten Arrays genutzt werden. In diesem Fall werden also die beiden rele- vanten Schlüssel als Eigenschaftsnamen verwendet. Vielleicht fragen Sie sich gerade, welchen Vorteil diese Vorgehensweise bringt, da man doch auch direkt auf das Array zugreifen könnte. Nun, der erste Punkt ist, dass die Eigenschaften des Objekts, also die Konfigurationsdaten, nur gelesen, nicht aber geschrieben werden können. Somit können die Konfigurationsdaten zur Laufzeit nicht verfälscht werden. Möchten Sie Änderungen zur Laufzeit zulas- sen, so übergeben Sie als zweiten Parameter den booleschen Wert true. Der zweite Vorteil ist, dass Sie den Code bei dieser Vorgehensweise jederzeit von einem Array auf die Nutzung einer Datei umstellen können. Sollten Sie nicht so direkt auf die Eigenschaften zugreifen können oder wollen, so können Sie die Daten auch mit einer foreach-Schleife durchlaufen, da die Klasse das SPL-Interface Iterator implementiert. Die Anzahl der Konfigurations- werte in einer Ebene lässt sich jederzeit durch den Aufruf der Methode count() auslesen. Ein Problem bei der Nutzung von Konfigurationsdateien ist die Frage, ob alle Werte gesetzt wurden, beziehungsweise wie Sie Default-Werte in das System integrieren. Versuchen Sie, einen Wert auszulesen, der nicht gesetzt ist, so stellt dies kein Problem dar. Die Klasse wirft keine Exception o.Ä., sondern liefert Ihnen nul 1 zurück, wenn Sie auf die Eigenschaft zugreifen. So könnten Sie auf dieser Basis schnell und einfach mithilfe einer i f-Abfrage einen Default-Wert vorsehen. Allerdings geht das auch einfacher. Die Klasse kennt die Methode get(), welche zwei Parameter übergeben bekommt. Der erste ist hierbei die Eigenschaft bzw. der Konfigurationswert, den Sie auslesen wollen. Der zweite Parameter ist der Default-Wert, den die Methode liefert, falls die gewünschte Eigenschaft nicht mit einem Wert belegt wurde: // Ableiten des Objekts und Übergabe der Daten Sconfig = new Zend_Config ($conf_daten); // $db_typ ist jetzt null $db_typ = $config->db_typ: if (null == $db_typ) { $db_typ = ’mysql’: } 196
e-bol.net Konfigurationsverwaltung Zend_Config | 4-6 // user ist nicht gesetzt // => $user bekommt root zugewiesen $user = $config->get('user'root'); // password ist gesetzt, allerdings null // => $password wird null zugewiesen Spassword = $config->get('password','geheim'); Listing 4.23 Nutzung von Default-Werten 4.6.2 INI-Dateien INI-Dateien stellen quasi einen Industriestandard für Konfigurationsdateien dar und werden oft genutzt. Der Aufbau einer solchen Datei ist einfach. Es gibt ver- schiedene Abschnitte, die einen Namen haben, der jeweils von eckigen Klammen eingeschlossen ist. Danach können Sie einzelnen Bezeichnern, deren Namen nur aus den Buchstaben von A-Z in Groß- und Kleinschrift sowie dem Unterstrich bestehen sollen, Werte zuweisen. Die Zuweisung erfolgt mithilfe eines Gleich- heitszeichens. Möchten Sie eine INI-Datei kommentieren, ist dies kein Problem. Nutzen Sie ein- fach ein Semikolon am Zeilenanfang, um einen einzeiligen Kommentar einzulei- ten. Möchten Sie einer Konfigurationsvariablen einen Wert zuweisen, der Zeichen enthält, die nicht alphanumerisch sind, wie beispielsweise Slashes oder Ähnli- ches, so muss der Wert in Anführungszeichen gesetzt werden. Des Weiteren müssen Sie auch beachten, dass die Werte yes und true in die Zahl 1 konvertiert werden. Möchten Sie den String yes bzw. true zuweisen, müssen Sie die Texte in Anführungszeichen setzen. Die Werte null, no und fal se hinge- gen werden in einen leeren String konvertiert. In dem folgenden Beispiel wird die Datei config.ini genutzt, welche die folgen- den Daten enthält: [dbserver] host = localhost user = root password = geheim pwd_nutzen = yes [pfade] bilder = "/var/www/img/"; 197
e-bol.net 4 | Infrastruktur-Klassen [text] ; Hier bitte die lokalisierten Texte eingeben willkommen = "Datenbankverbindung wurde aufgebaut" Listing 4.24 Aufbau einer Konfigurationsdatei Diese INI-Datei enthält drei Abschnitte, sogenannte Sections. Um die Datei zu nutzen, übergeben Sie dem Konstruktor der Klasse Zend_Config_Ini den Pfad und den Namen der Datei sowie als zweiten Parameter die Information, welche Sektionen genutzt werden sollen. Hier können Sie einen String übergeben, wenn nur einer der Abschnitte genutzt werden soll, ein Array mit mehreren Namen, wenn mehr als ein Abschnitt genutzt werden soll, oder null, falls alle Sections relevant sind. requi re_once 'Zend/Confi g/Ini.php'; Sdatei = 'config.ini ’; try { // Ableiten des Objekts und Übergabe der Daten Sconfig = new Zend_Config_Ini (Sdatei, null); // pwd_nutzen wurde yes zugewiesen // somit enthält die Eigenschaft den Wert 1 if (1 == $config->dbserver->pwd_nutzen) { Spassword = $config->dbserver->password; el se { Spassword = ’': Suser = $config->dbserver->user; Shost = $config->dbserver->host; Serg = mysql_connect(Shost, Suser, Spassword); if (false != Serg) { echo $config->text->wil1 kommen; } catch (Zend_Config_Exception Se) { die (Se->getMessage()); } Listing 4.25 Nutzung einer INI-Datei 198
e-bol.net Konfigurationsverwaltung Zend_Config | 4-6 Wie Sie sehen, ist der Zugriff auf die Konfiguratio ns werte identisch mit der Array-Variante. Somit sind keine großen Besonderheiten zu beachten. Einen klei- nen Fallstrick gibt es aber dennoch. Sollten Sie eine Konstante definiert haben, deren Name identisch ist mit einem der zugewiesenen Werte, wird der Wert durch die Konstante ersetzt. Wenn also in der Konfigurationsdatei die folgenden Zeilen [dbserver] host = localhost enthalten sind, und in dem PHP-Code, der die Konfiguration einliest, das fol- gende Statement define ('localhost','127.0.0.1'); steht, so resultiert das darin, dass $config->dbserver->host den Wert 127.0.0.1 enthält und nicht localhost. Dieses Verhalten kann möglicherweise für etwas Verwirrung sorgen. Wie eingangs erwähnt, können Sie dem Konstruktor den Namen einer Section übergeben, die interpretiert werden soll. Leider hat das zur Folge, dass das Ver- halten der Klasse sich ein wenig ändert. Wie Sie schon gesehen haben, wird der Name einer Section als Eigenschaft bzw. als Hierarchieebene genutzt. Übergeben Sie explizit, welche Abschnitte genutzt werden sollen, so entfällt diese Zwischen- ebene. Würde dem Konstruktor ein Array mit den Namen der Sektionen über- geben, müsste der Zugriff folgendermaßen erfolgen: Ssektionen = array ('dbserver', 'pfade', 'text'); Sconfig = new Zend_Config_Ini (Sdatei, $sektionen); $user = Sconfig->user; $host = $config->host; Listing 4.26 Nutzung von Sektionen Vor diesem Hintergrund sollten Sie im Vorfeld genau planen, wie Sie Ihre Konfi- guration aufbauen wollen. Möchten Sie die Namen der Sektionen übergeben, aber trotzdem etwas mehr Struktur in Ihre Konfiguration bringen, so haben Sie die Möglichkeit, die Bezeichner zu verschachteln. Hierbei trennen Sie einfach die verschiedenen Namensteile mithilfe eines Delimiters (standardmäßig ein Punkt) ab. Wenn Sie in einer Section, die Sie explizit einlesen, die Zeile db.name = local host angeben, können Sie im PHP-Code auf $config->db->name zugreifen. Möchten einen anderen Delimiter als den Punkt nutzen, ist dies auch kein Problem. Sie können dies mithilfe des dritten Parameters des Konstruktors beeinflussen. An 199
e-bol.net 4 | Infrastruktur-Klassen dieser Stelle können Sie ein Array übergeben, welches die Schlüssel nestSepa ra- tor und al 1owModifications unterstützt. Mit dem ersten übergeben Sie ein alternatives Trennzeichen, wohingegen der zweite einen booleschen Wert unter- stützt, der festlegt, ob die eingelesenen Konfigurationswerte verändert werden dürfen. Dieses Verhalten können Sie übrigens auch dadurch steuern, dass Sie als dritten Parameter lediglich einen booleschen Wert übergeben. Ein interessantes Feature ist, dass Zend_Confi g_Ini bestimmte Konfigurations- abschnitte mit anderen überschreiben kann. Damit haben Sie die spannende Möglichkeit, einen Teilbereich schnell und einfach durch einen anderen zu erset- zen. Somit können Sie beispielsweise völlig unproblematisch zwischen zwei Datenbankservern umschalten. In der folgenden INI-Datei finden Sie zwei Sec- tions. Die erste enthält die Daten der Produktivdatenbank, die zweite die Daten des Entwicklungsservers: [dbserver] host = localhost db = umsaetze user = root password = geheim [entwicklung:dbserver] db = test password = totalgeheim Wird nun beim Ableiten des Objekts der String entwicklung als Section-Name angegeben, so werden die Abschnitte mit dem Präfix entwicklung eingelesen. Der eigentliche Section-Name, der nach einem Doppelpunkt folgt, sorgt dafür, dass auch andere Abschnitte mit diesem Namen eingelesen werden. Konfigurati- onswerte, die doppelt vorhanden sind, werden allerdings durch die explizit ange- gebene Sektion überschrieben: // Neues Objekt fuer die Entwicklung Sconfig = new Zend_Config_Ini (Sdatei, 'entwicklung'); echo $config->host: //Gibt localhost aus echo $config->db; // Gibt test aus 4.6.3 XML-Dateien Neben den schon erwähnten INI-Dateien können Sie auch mit XML-Dateien arbeiten. Auch wenn der Aufwand, eine XML-Datei zu erstellen, etwas größer ist, so bietet das System doch auch ein wenig mehr Sicherheit und Flexibilität. Soll- ten Sie noch nicht sicher im Umgang mit XML sein, würde ich Ihnen empfehlen, 200
e-bol.net Konfigurationsverwaltung Zend_Config | 4-6 dass Sie erst ein XML-Tutorial lesen, bevor Sie diese Technik bei Konfigurations- dateien einsetzen. Die Arbeit mit XML-Dateien ist weitgehend identisch mit der Nutzung von INI- Dateien, wobei hier spezielle Ausdrücke wie yes, no, true und false natürlich nicht konvertiert werden. Wichtig ist allerdings die Information, dass die Klasse Zend_Config_Xml im Hin- tergrund auf SimpleXML aus PHP zugreift. Standardmäßig ist diese Erweiterung bei einer PHP 5-Installation vorhanden. Sollte das aber einmal nicht der Fall sein, so funktioniert Zend_Conf1g_Xml nicht. Wie auch schon bei Zend_Confi g_I ni geben Sie als ersten Parameter den Namen der Datei mit den Konfigurationsdaten an. Der zweite Parameter, der auch in die- sem Fall obligatorisch ist, gibt wieder an, welche Sektionen der Konfiguration interpretiert werden sollen. Die Namen der Sektionen können Sie auch hier als String oder als Array angeben, wobei die Angabe von nul 1 dafür sorgt, dass alle Abschnitte genutzt werden. Bei einer XML-Datei stellt sich dabei die Frage, was eine Sektion ist. Zend_Config_Xml geht hierbei davon aus, dass es ein Root-Ele- ment gibt. Die darauffolgenden Kind-Elemente fassen die Sektionen zusammen, die dann wiederum diejenigen Elemente enthalten, welche die eigentlichen Daten enthalten: <root> <sekti onl> < ei genschaft>wert</ei genschaft> < ei genschaftl>wert</ei genschaft> </se kt ionl> <sekti on2> < !-- weitere Daten --> </se kt ion2> </root> Listing 4.27 Aufbau einer XML-Konfigurationsdatei Der Konstruktor akzeptiert also nur Namen von Elementen der ersten Ebene (in diesem Fall sekti onl und sekti on2) als Abschnittsnamen. Auch hier gilt wieder, dass Sie beim Zugriff auf die Eigenschaften die Namen der Sektionen angeben müssen, falls Sie als zweiten Parameter null angegeben haben. Sollten Sie den oder die Namen der Sektion(en) explizit beim Ableiten des Objekts angegeben haben, so entfällt dieser beim Zugriff auf die Eigenschaften. Bezogen auf das obige Beispiel müssten Sie also, falls Sie das Objekt wie folgt $obj = new Zend_Config_Xml(’datei.xml', ’sektionl'); 201
e-bol.net 4 | Infrastruktur-Klassen ableiten, so auf die Eigenschaften zugreifen: echo $obj->eigenschaft: Sollten Sie das Objekt allerdings in dieser Form $obj = new Zend_Config_Xml(’datei.xml', null): abgeleitet haben, würde der korrekte Zugriff wie folgt aussehen: echo $obj->sektionl->eigenschaft: Möchten Sie die Daten noch weiter verschachteln, ist dies kein Problem. Zwi- schen Sektions- und Daten-Tags können Sie noch Zwischenebenen einfügen. In dem folgenden Beispiel wird die nachfolgende XML-Datei zugrunde gelegt: <?xml version="l.0" encoding="UTF-8"?> <confi g> <!-- Hier kommt die Konfiguration --> <dbserver> <host>localhost</host> <user>root</user> <password></password> </dbserver> <entwicklung extends='dbserver’> <host>192.168.0.2</host> </entwicklung> </confi g> Sie sehen hier, dass das Element entwicklung noch um das Attribut extends = 'dbserver' ergänzt wurde. Hiermit ist es (wie auch schon bei den INI-Dateien) möglich, bestimmte Elemente einer anderen Sektion zu überschreiben. requi re_once 'Zend/Confi g/Xml.php'; Sdatei = 'config.xml’; try { // Ableiten des Objekts und Übergabe der Daten // Sektion entwicklung wird geladend und lädt // automatisch dbserver nach. tconfig = new Zend_Config_Xml ($datei, 'entwicklung'); $host = $config->host: // ist 192.168.0.2 tuser = $config->user; tpassword = $config->password; $db = mysql_connect($host, $user, $password): } catch (Zend_Config_Exception $e) ( 202
e-bol.net Shell-Programmierung mit Zend_Console_Getopt | 4-7 die ($e->getMessage()): I Listing 4.28 Einlesen einer XML-Datei Auch hier haben Sie wieder die Möglichkeit, zwischen einer oder mehreren Kon- figurationen umzuschalten, indem Sie den Namen einer anderen Sektion überge- ben. 4.7 Shell-Programmierung mit Zend_Console_Getopt Sicher kennen Sie auch das Problem, dass es irgendwelche Befehlsketten auf dem Computer gibt, die Sie immer und immer wieder eintippen müssen. Auch wenn dies auf dem heimischen Rechner nicht so oft passiert, so ist das auf Servern doch ein ganz alltägliches Übel. An dieser Stelle greift der geübte Administrator schnell zu einem Bash- oder Perl- Script. Sollte es Ihnen aber gehen wie mir und »sprechen« Sie besser PHP als eine der anderen Sprachen, so fällt es Ihnen sicher auch leichter, ein solches Pro- gramm mit PHP zu erstellen. Sollten Sie sich noch nie mit dieser Thematik beschäftigt haben, empfehle ich Ihnen, einen Blick auf das entsprechende Kapitel des PHP-Manuals zu werfen. Die entsprechende Seite finden Sie unter http://de.php.net/manual/de/features.commandline.php. Eines der Probleme, mit dem man in diesem Zusammenhang oft zu tun hat, ist die Übergabe von Parametern an das Script. Zwar stellt PHP in der Variablen $_SERVER[' argv' ] alle Werte zur Verfügung, die in der Kommandozeile an das Script übergeben wurden, aber der Aufwand, diese Daten auszuwerten, ist recht hoch. Um diesen Vorgang zu vereinfachen ist die Klasse Zend_Console_Getopt entwickelt worden. Um mit Zend_Consol e_Getopt zu arbeiten, sollten Sie sich zunächst mit den hier genutzten Begriffen anhand der folgenden Zeile vertraut machen: ssh -1 root 192.168.0.1 Der Befehl ssh, mit dem Sie eine Verbindung zu einem anderen Server aufbauen können, erhält hier verschiedene Werte über die Kommandozeile übergeben. Die einzelnen Bestandteile werden dabei mit Leerzeichen getrennt. Bei der ers- ten Information, dem -1 root, handelt es sich um eine Option. Diese Option setzt sich aus zwei Teilen zusammen: dem Flag -1 und dem Parameter root. Hier wird dem Befehl ssh ein String (root) übergeben, wobei das Flag (-1) ihn darü- ber informiert, was der String bedeutet. 203
e-bol.net 4 | Infrastruktur-Klassen Bei der zweiten Information, der Angabe 192.168.0.1, handelt es sich um ein Argument. Argumente sind eigenständige Informationen, die ohne ein vorange- stelltes Minuszeichen genutzt werden. Welche Information von einem Argument transportiert wird, erkennt ein Befehl üblicherweise daran, an welcher Stelle in der Reihenfolge das Argument übergeben wurde. Der letzte Begriff, den Sie kennen sollten, ist Cluster. Bei einem Cluster handelt es sich um mehrere Flags, die zusammengezogen und nur mit einem Minuszei- chen eingeleitet werden. Wenn Sie beispielsweise anstelle von 1 s - a -1 - h also somit nur 1 s -al h eintippen. In dem Fall handelt es sich bei al h um ein Cluster von Flags. 4.7.1 Optionen, Flags und Parameter So viel zur Theorie. Lassen Sie uns nun in die Praxis einsteigen. Der einfachste Weg, der Klasse mitzuteilen, welche Flags Sie erwarten, besteht darin, dem Kon- struktor eine Liste mit Flags als Parameter zu übergeben. Diese Flags können Sie dabei einfach als String übergeben. #! /usr/bi n/php < ? php requi re_once 'Zend/Console/Getopt.php'; Sopts = new Zend_Console_Getopt('up'); // Hier werden die Parameter verarbeitet.... ?> Listing 4.29 Übergabe von Flags In diesem Beispiel akzeptiert das Script die beiden Flags u und p. Sie können diese also optional beim Aufruf des Scripts angeben. In dieser Variante können Sie allerdings keine Werte übergeben. Möchten Sie ein Flag deklarieren, mit dem eine Option übergeben werden kann, so setzen Sie einen Doppelpunkt hinter den Buchstaben des Flags. In diesem Fall würden Sie also " u: p:" als Parameter übergeben. Um die Daten auszulesen, können Sie auf die Eigenschaften u und p des Objekts Sopts zugreifen, oder Sie lesen die Werte mit der Methode getOp- tion() aus. Diese bekommt den Namen des Flags übergeben: #! /usr/bi n/php < ? php requi re_once 'Zend/Console/Getopt.php'; Sopts = new Zend_Console_Getopt(’u:p:qr'); echo "Wert von u: ".$opts->u."\n"; echo "Wert von p: ".Sopts->getOption(*p’)."\n"; echo "Wert von q: ".Sopts->getOption('q’). "\n"; 204
e-bol.net Shell-Programmierung mit Zend_Console_Getopt | 4-7 echo "Wert von r: ".$opts->getOption('r'). "\n"; ?> Rufen Sie dieses Script also über die Kommandozeile mit Skript.php -u root -p geheim -q auf, erhalten Sie die folgende Ausgabe: Wert von u: root Wert von p: geheim Wert von q: 1 Wert von r: Hier werden also q und r wirklich als reine Flags betrachtet. Selbst wenn Sie einen Wert anhängen, wird dieser ignoriert. Für Flags, die beim Aufruf nicht übergeben wurden, wie das Flag r in diesem Beispiel, liefert die Klasse null zurück. Wichtig ist, dass ein Flag, bei dem Sie einen Doppelpunkt angefügt haben, stets einen Parameter haben muss. Ist der Parameter nicht vorhanden, wirft die Klasse eine Exception. Sie sollten also immer auf ein korrektes Exception Handling ach- ten. Mit diesen Möglichkeiten können Sie sicher schon einiges erreichen; aber die Klasse kann noch mehr. Viele Befehle kennen die Möglichkeit, anstelle von ein- fachen Flags auch eine Langschreibweise zu benutzen. So können Sie beispiels- weise bei dem Befehl grep -c oder - -count angeben, um den Befehl zu veranlas- sen, Treffer zu zählen. Die längere Schreibweise ist oft einfacher zu erlernen. Ebenso ist bei langen Befehlszeilen besser zu erkennen, was ein Befehl machen soll. Eine solche Deklaration könnte so aussehen: requi re_once ’ Zend/Console/Getopt.php'; try { // Objekt ableiten $opts = new Zend_Console_Getopt( array ( ’user|u=w' => 'Username', ’password|p=s' => 'Passwort', ’quit|q'=>'Verbindung nach Befehl beenden')); // Ausgabe der übergebenen Werte echo "Wert von u: ".$opts->u. "\n"; echo "Wert von p: ".$opts->getOption('p’)."\n"; echo "Wert von q: ".$opts->getOption('q’)."\n"; 205
e-bol.net 4 | Infrastruktur-Klassen catch (Zend_Console_Getopt_Exception $e) { echo $e->getUsageMessage(): } Listing 4.30 Nutzung der Langschreibweise In diesem Fall wurde dem Konstruktor ein Array übergeben. Jeder Schlüssel beinhaltet die Lang- und die Kurzschreibweise des entsprechenden Flags, die durch ein Pipe-Symbol (|) getrennt werden. Nach der Kurzschreibweise des Flags folgt in zwei Fällen noch die Information, dass ein Wert erwartet wird und wel- che Art von Wert akzeptiert wird. Ein Doppelpunkt an dieser Stelle ist unzuläs- sig. Das =w steht dafür, dass ein »Wort«, also ein String ohne Leerzeichen, erwar- tet wird. Die Angabe =s bedeutet, dass das Flag einen String erwartet. Hier können Sie ein Wort übergeben, aber auch einen String, der Leerzeichen enthält, sofern Sie ihn in Anführungszeichen einfassen. Das würde bei =w in einer Excep- tion resultieren. Außerdem können Sie auch noch =i für einen Integer-Wert angeben. Ein Platzhalter für einen Float-Wert ist nicht vorgesehen, wobei die Notwendigkeit im Bereich der Shell-Programmierung wohl eher gering ist. Diese Platzhalter sorgen stets dafür, dass ein Wert obligatorisch ist, also angege- ben werden muss. Das bezieht sich allerdings nur auf den Wert. Geben Sie das Flag nicht an, ist das kein Problem. Wird das Flag aber angegeben, so müssen Sie auch zwingend einen Parameter angeben. Soll ein Wert optional sein, so ersetzen Sie das Gleichheitszeichen einfach durch ein Minus-Zeichen. Bei den Werten, die in dem Array genutzt wurden, handelt es sich um eine Erläu- terung, die kurz beschreibt, wozu das Flag benötigt wird oder welchen Wert es erwartet. Diese Erläuterungen können Sie als Hilfetexte nutzen, was in Listing 4.30 im Exception Handling geschieht. Der Catch-Block verarbeitet eine Excep- tion vom Typ Zend_Consol e_Getopt_Excepti on, welche die Methode getUser- Messaget) kennt. Sie liefert den Hilfetext zurück, sodass Sie ihn ausgeben kön- nen. In diesem Beispiel würde die Ausgabe folgendermaßen aussehen: Usage: Skript.php [ options 1 --user|-u <word> Username --password|-p <string> Passwort --quit|-q Verbindung nach Befehl beenden Sie können an dieser Stelle auch auf die Methode getMessaget) zurückgreifen. Diese liefert Ihnen eine Fehlermeldung, welches Flag nicht korrekt genutzt wurde. Ich möchte nicht unerwähnt lassen, dass Sie die Kurz- und die Langschreibweisen bei der Festlegung der Flags auch kombinieren können. Die Kurzschreibweise 206
e-bol.net Shell-Programmierung mit Zend_Console_Getopt | 4-7 bleibt in dem Fall unberührt, die Flags werden also direkt an den Konstruktor übergeben. Die Flags in der Langschreibweise werden dann als Array an die Methode addRules() übergeben und auf diesem Weg hinzugefügt. Damit die Nutzung der Flags in Kurzschreibweise auch in der Hilfe auftaucht, können Sie entsprechende Erläuterungen mithilfe von setHel p() hinzufügen: Sopts = new Zend_Console_Getopt('u:'): Sopts->setHelp(array( ’u' => ’Username'): 4.7.2 Nutzung von Argumenten Um Argumente auszulesen, können Sie auf die Methode getRemainingArgs() zurückgreifen. Sie liefert Ihnen ein Array mit allen Kommandozeilenwerten zurück, die keinem Flag zugeordnet werden konnten: requi re_once ’Zend/Console/Getopt.php1; try { Sopts = new Zend_Console_Getopt('u:1:'): Sopts->setHelp(array (’u' => 'Username’, ’p' => 'Passwort')): // Alles auslesen was nicht zu den Flags gehört Sargs = Sopts->getRemainingArgs(): if (true —== empty(SargsEO])) { // Exception bekommt zuerst die normale Message und dann // die Usage-Message übergeben throw new Zend_Console_Getopt_Exception( 'Geben Sie einen Host an’, 'Nutzung: skript.php -u <User> -p <Passwort> Host'): Sserver = SargsEO]; // Weitere Verarbeitung 1 catch (Zend_Console_Getopt_Exception Se) { echo Se->getUsageMessage(). "\n"; echo Se->getMessage(). "\n"; 1 Listing 4.31 Auslesen von Argumenten 207
e-bol.net 4 | Infrastruktur-Klassen Zend_Consol e_Getopt ist eine kleine, aber sehr praktische Klasse, wie ich finde. Über die hier genannten Möglichkeiten hinaus gibt es noch einige weitere. So können Sie die übergebenen Werte beispielsweise auch im XML- oder im JSON- Format auslesen. 208
e-bol.net Prinzipien sind der jämmerlichste Grund, den es gibt, um sich unbeliebt zu machen. - George Bernhard Shaw 5 Webservices 5.1 Feeds mit Zend_Feed verarbeiten Im Zeitalter der Blogs sind Feeds für viele Menschen eine wichtige Informations- quelle. Bei Feeds handelt es sich um Informationen, die auf Basis von XML zur Verfügung gestellt werden. Informationen heißt an dieser Stelle, dass es meist Daten wie Blogeinträge, Teaser von Zeitungsartikeln oder Ähnliches sind. In den meisten Fällen handelt es sich um eine Überschrift sowie einen kurzen Text, die teilweise mit einem Bild ergänzt werden. Üblicherweise sind auch ein Link auf den eigentlichen Artikel sowie ein Publikationsdatum enthalten. Feeds haben gegenüber dem eigentlichen Blog den Vorteil, dass man sie gut in Feedreadern zusammenfassen kann. Dabei handelt es sich um Programme oder Websites, die verschiedene Feeds einlesen, aufbereiten und darstellen, sodass man sich schnell einen Überblick über die neuen Einträge verschaffen kann. Feeds werden in verschiedenen Formaten angeboten, wobei die Zend-Klassen mit RSS- und Atom-Feeds umgehen können. Bitte beachten Sie bei der Arbeit mit Feeds, dass Sie immer ein Exception Hand- ling implementieren sollten. Bei der Arbeit mit solchen Informationsangeboten kann es nämlich schnell passieren, dass Sie nicht auf eine Ressource zugreifen können. 5.1.1 Feeds finden In den meisten Fällen wird es sicher so sein, dass Sie die URL eines Feeds kennen bzw. von einer Website übernehmen können. Allerdings gibt es auch oft die Möglichkeit, die URLs der Feeds, die zur Verfügung stehen, automatisiert zu übernehmen. Dazu müssen die URLs der Feeds mithilfe des Tags <l 1 nk> auf der eigentlichen Webseite vermerkt sein. Zend_Feed ist in der Lage, diese Tags zu fin- den und die dort enthaltenen URLs zu extrahieren. 209
e-bol.net 5 | Webservices Die gefundenen Feed-URLs werden dann direkt eingelesen. Die Methode gibt Ihnen ein Array mit Feed-Objekten zurück. Leider ist momentan keine Möglich- keit vorgesehen, die URL des Feeds auszulesen. Die Methode f i ndFeeds() ist statisch deklariert, sodass Sie nicht erst ein Objekt ableiten müssen: require_once ’Zend/Feed.php’; try { $feeds = Zend_Feed::findFeeds('http://www.mygadgetblog.de'): 1 catch (Exception $e) { die ($e->getMessage()): 1 if (0 === count($feeds)) { echo "Es wurden keine Feeds gefunden": 1 el se { //Hier können die Feeds verarbeitet werden 1 Listing 5.1 Automatisches Finden von Feeds 5.1.2 Allgemeines zur Verarbeitung von Feeds Unabhängig davon, mit welcher Art von Feed Sie es zu tun haben, gibt es zwei Möglichkeiten, die Feeds einzulesen. Die erste Variante, die auch nachfolgend genutzt wird, ist ein Objekt der entsprechenden Klasse abzuleiten und dem Kon- struktor die URL des Feeds zu übergeben. Die zweite Variante ist, dass Sie die sta- tische Klasse import() aus der Klasse Zend_Feed aufrufen und ihr die gewünschte URL übergeben. Sie ermittelt dann selbstständig die Art des Feeds und liefert ein Objekt der entsprechenden Klasse zurück. Auch wenn Importe) komfortabler erscheint, stellt sich dabei unter Umständen doch das Problem, dass nicht ganz klar ist, ob ein Atom- oder ein RSS-Feed eingelesen wird. Daher gebe ich in den folgenden Beispielen dem expliziten Ableiten der Objekte den Vorzug. Ist der Feed eingelesen, wird er mithilfe der PHP-DOM-Funktionen aufbereitet. Um auf ein Element, also den Textinhalt eines XML-Tags, zuzugreifen, nutzen Sie einfach den Namen des Tags als Methodennamen. Der Inhalt wird dann direkt als String zurückgegeben: $feed->title() 210
e-bol.net Feeds mit Zend_Feed verarbeiten | 5-1 Ist das Element nicht vorhanden, gibt der Methodenaufruf null zurück. Attribute von Tags lesen Sie aus, indem Sie den Namen des Attributs als Array- Schlüssel an den Namen des Elements anhängen. In dem Fall wird der Name des Arrays nicht als Methode, sondern nur als Array-Name genutzt. Wollen Sie den Wert des href-Attributs eines Elements namens link auslesen, würde das so aus- sehen: $feed- >1i nk[’href'] Auch wenn Sie nur Atom- oder nur RSS-Daten verarbeiten wollen, sollten Sie beide Abschnitte lesen, da die dortigen Informationen sich auf beide Fälle bezie- hen. 5.1.3 Verarbeiten von RSS-Feeds RSS ist das Format für Feeds, das sicher die größte Verbreitung hat. Auch wenn RSS als veraltet gilt, so ist es auf absehbare Zeit nicht wegzudenken. Das Problem ist, dass bei einem RSS-Feed, schon alleine aufgrund der Vielzahl der unter- schiedlichen Unterformate, nicht ganz genau gesagt werden kann, welche Ele- mente vorhanden sind und welche nicht. Daher kann ich an dieser Stelle nicht dafür garantieren, dass alle Elemente, die in dem Beispiel genutzt werden, auch später bei einem Feed verfügbar sind, den Sie einlesen wollen. Am einfachsten und sichersten ist es, wenn Sie einen Blick auf die XML-Daten werfen, die der Feed zur Verfügung stellt. Dann kann der Feed sich zwar später noch ändern, aber Sie haben zunächst einmal einen guten Anhaltspunkt. Die Verarbeitung eines RSS-Feeds könnte so umgesetzt werden: < ? php requi re_once 'Zend/Feed/Rss.php’; header('Content-Type: text/html; charset=utf-8'): try { // Feed einlesen $feed = new Zend_Feed_Rss( 'http://www.mygadgetblog.de/index.php/feed/'); I catch (Exception $e) { die ($e->getMessage()): } // Titel des Blogs bzw. Feeds ausgeben echo ’<p>Titel des Blogs: '.$feed->title(): 211
e-bol.net 5 | Webservices // Anzahl der Einträge ermitteln echo ’<br>Anzahl der Eintr&auml;ge: '.$feed->count().'</p>'; echo ’<table>'; // Über die Beiträge iterieren foreach ($feed as $item) { echo '<tr><td>'; if ($item->link() != null) { echo ’<a href="$item->link( ; echo $item->title(); echo ’</a>’; el se { echo $item->title(); echo '</td></tr>'; echo ' <trXtd>'; if ($item->description() != null) { echo $item->description(); el se { echo 'Keine Beschreibung vorhanden'; echo ’</tdX/tr>'; echo '<trXtd>&nbsp;</tdX/tr>’; } echo ’</table>'; Listing 5.2 Verarbeiten eines RSS-Feeds Wie Sie in diesem Listing sehen, ist die Nutzung der Klasse denkbar einfach. Nach dem Einlesen des Feeds stehen die Informationen, die unterhalb des Chan- nel -Elements deklariert wurden, direkt zur Verfügung. Somit kann also beispiels- weise der Titel des Feeds durch den Aufruf der Methode titlet) ausgelesen werden. Die Methode count (), die die Anzahl der Einträge liefert, ist übrigens in der Klasse definiert. Es handelt sich dabei nicht um ein Element aus dem Feed. Um alle Einträge auszulesen, können Sie das Objekt direkt in einer foreach- Schleife verwenden, da die Klasse das SPL-Interface Iterator implementiert. Bei dem Objekt, das Sie bei jedem Durchlauf erhalten, handelt es sich um ein item- Element aus den XML-Daten. Alle Kind-Elemente, die unterhalb von item dekla- 212
e-bol.net Feeds mit Zend_Feed verarbeiten | 5-1 riert sind, stehen dann wieder als Methoden zur Verfügung. Bitte beachten Sie, dass Sie auf jeden Fall auf die Methoden zugreifen sollten. Greifen Sie auf die Eigenschaften zu, so würde das zwar auch funktionieren, solange im Feed ein XML-Element mit dem entsprechenden Namen vorhanden ist. Ist dieses nicht vorhanden, liefert der Zugriff auf die Eigenschaft ein leeres DOM-Element zurück. Die Methode hingegen gibt null zurück, was in einer i f-Abfrage leichter zu testen ist. Dabei sollten Sie beachten, dass die Methode nur dann null zurück- gibt, wenn das Element nicht vorhanden ist. Ist das Element leer, gibt die Methode einen leeren String zurück. In dem obigen Beispiel wird diese Eigen- schaft genutzt. In einem normalen Vergleich werden ein leerer String und null als gleich einge- stuft. Die i f-Abfragen im Körper der Schleife testen somit, ob das Element, das abgefragt werden soll, im Feed vorhanden bzw. möglicherweise leer war. Der Titel eines Elements wird sicher immer vorhanden sein, weswegen ich an der Stelle auf eine weitere Abfrage verzichtet habe. Die XML-Daten aus dem Feed, die hier genutzt wurden, haben die nachfolgende Struktur: <?xml version="l.0" encoding="UTF-8"?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modul es/content/" xmlns:wfw="http://wel1formedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.l/"> <channel> <title>myGadgetßlog</title> <1ink>http://www.mygadgetblog.de</li nk> <1tem> <title>LED am Einsatzwagen: so muss das :)</title> <1 i nk>http://www.mygadgetblog.de/i ndex.php/1ed-am- ei nsatzwagen-so-muss-das/</link> <description>Und weil ja bald Weihnachten...</description> </i tem> <i tem> <title>Fotos vom Philips Fizz sehr beiiebt</title> <1 i nk>http://www.mygadgetblog.de/index.php/fotos-vom-philips- fizz-sehr-beiiebt/</li nk> <description>Offenbar habe ich das Foto .... </description> </i tem> <!-- weitere Items --> </channel> </rss> 213
e-bol.net 5 | Webservices Diese XML-Daten sind stark gekürzt und sollen nur helfen, den PHP-Code besser verständlich zu machen. In einem echten Feed sind natürlich auch noch Datums- und Sprachinformationen und Ähnliches vorhanden. Wie der fertig aufbereitete Feed im Browser dargestellt wird, sehen Sie in Abbil- dung 5.1. http://12 7.0.0.1/zf-buch/feed/l.php Q.Q.00I© *^0.* Google pi~j rrJ Titel des Blogs: myGadgetBlog Anzahl der Einträge: 30 LED am Einsatzwagen: so muss das :) Und weil ja bald Weihnachten ist, und da alles so festlich beleuchtet wird, hier mal ein Beispiel, wie man ein Einsatzfahrzeug beleuchtet, dass es auch wirklich auffällt *schmacht* Social Bookmarks: Fotos vom Philips Fizz sehr beliebt Offenbar habe ich das Foto von meinem ersten Handy, dem Philips Fizz, gut getroffen. Kürzlich wunderte ich mich über eine sprunghaft gestiegene Zahl von Zugriffen auf das Blog. Also habe ich mir mal die Log-Files angeschaut und festgcstellt, dass 90 Prozent der Besucher von ein und der selben Website zu mir kamen, also hab ich da [...] Trust Spyker Fl Wie vor ein paar Tagen geschrieben, hab ich ja auf der Wcihnachtstombola eine Drahtlose PC Funktmaus “Trust Spyker Fl" gewonnen. Nun kam ein Kollege um die Ecke, er hätte auch so eine Maus, ob den bei mir das Mausrad ginge;). Scheint ja ein besonderes Feature dieser Maus zu sein, dass es nicht geht. Also hab [...] Körpertyp Olivenöl? Arbeiten beim Fernsehen nur noch V*lldeppen? Das wäre wieder so ein Eintrag unter der Rubrik “Unglaublichkeiten und Unfassbares”. Marge Simpson sucht sich soeben eine Figur für ihren Avatar in einem Online-Rollenspicl aus, und klickt auf Körpertyp “Olivenöl” (Originalsynchronisation Pro7). Die meinen “Olivia Öl” - die Angebetete von Popeye dem Seemann. Meine Güte, wie schlecht! Social Bookmarks: Neue Maus: Trust Spyker Fl limited edition Heule war Tombola in der Firma, wie immer hab ich Alkohol gewonnen, wenn’s denn wenigstens mal Rotwein wäre aber gut, besser als die sonst übliche Salami Ein Kollege bekam eine Maus eine ” Trust Spyker Fl ” in einer limitierten Edition mit Unterschrift von irgendsonem Formel 1 Piloten, schickes Teil, aber irgendwie könnt [...] CD ATA mit PHP 5.1 und SimplcXML verarbeiten Weil ich kürzlich darüber gestolpert bin, dass ein XML-Dokument einen CDATA-Abschnitt Abbildung 5.1 Ausgabe des eingelesenen Feeds 5.1.4 Verarbeiten von Atom-Feeds Möchten Sie Daten aus einem Atom-Feed verarbeiten, ist das genau so einfach wie die Nutzung von RSS. Lediglich die Elemente haben andere Namen. Die Ver- arbeitung eines Atom-Feeds könnte beispielsweise so aussehen: 214
e-bol.net Feeds mit Zend_Feed verarbeiten | 5-1 < ? php requi re_once ’Zend/Feed/Atom.php'; header('Content-Type: text/html; charset=utf-8'); try { // Feed einlesen $feed = new Zend_Feed_Atom( 'http://www.adminblogger.de/blog/feed/atom/'); } catch (Exception $e) { die ($e->getMessage()): } // Titel ausgeben echo *<p>Titel: '.$feed->title(); echo ’ - ' .$feed->tagline(): // Anzahl der Einträge ermitteln echo ’CbOAnzahl der Eintr&auml ;ge: '.$feed->count().'</p>': echo "<table>"; // Über die Beiträge iterieren foreach ($feed as $item) { echo '<tr><td>': if ($item->link() !== null && $item->link[’href'1 != ’’) { echo ’<a href="$item->link['href; echo $item->title(); echo ’ </a> ’; el se { echo $item->title(); echo '</td></tr>'; echo '<trXtd>': echo 'Autor: '.$item->author->name(); echo ' </tdXtr>'; echo '<trXtd>': if ($item->summary() != null) { echo $item->summary(); el se 215
e-bol.net 5 | Webservices { echo 'Keine Beschreibung vorhanden'; echo '</td></tr>'; echo '<tr><td>&nbsp;</tdX/tr>’; } echo ’</table>'; Listing 5.3 Einlesen eines Atom-Feeds Wie Sie sehen, ist die grundsätzliche Struktur identisch mit der bei der Verarbei- tung eines RSS-Feeds. Daher möchte ich auch nur einige Kleinigkeiten genauer erläutern, wie beispielsweise die folgende i f-Abfrage: if ($item->1ink() !== null && $item->l1nk[’href'] != ’’) In Atom gibt es zwar ein Element namens link, aber die eigentliche Ziel-URL ist nicht als Text enthalten, sondern als Wert des Attributs href. Das heißt, der erste Teil der Abfrage prüft, ob das Element vorhanden ist. Wichtig ist hier, dass der »Nicht-Identitätsoperator« genutzt wird, da die Methode auf jeden Fall einen Leerstring zurückgibt, da link ein leeres Element ist. Mit dem zweiten Teil der Abfrage wird sichergestellt, dass das Attribut href vorhanden ist und einen Wert enthält. Wie am Anfang des Kapitels erwähnt, erfolgt der Zugriff auf Attribute dadurch, dass der Name des Elements nicht als Methode, sondern als Eigenschaft genutzt wird, die ein Array enthält. Der Schlüssel ist in diesem Fall der Name der Eigenschaft, die ausgelesen werden soll. Sollte das Attribut nicht vorhanden sein, liefert dieser Zugriff einen Leerstring zurück, generiert aber keine Notice oder einen anderen Fehler. Der nächste interessante Punkt ist der Zugriff auf den Namen des Autors. Hier nutzt Atom zwei ineinander verschachtelte Elemente, nämlich author und name. In diesem Beispiel erfolgt der Zugriff über: echo 'Autor: '.$item->author->name(); Das äußere Element author wird als Eigenschaft und nicht als Methode genutzt. Korrekterweise hätte man hier mit einer i f-Abfrage vorher testen müssen, ob das Element vorhanden ist. Die 1 f-Abfrage müsste in dem Fall so aufgebaut sein: if ($item->author() !== null && $item->author->name() !== null) Mit dieser Abfrage wird also zunächst geprüft, ob das Element author vorhanden ist. Mit der zweiten Abfrage wird dann sichergestellt, dass das verschachtelte Ele- ment name vorhanden ist. 216
e-bol.net Feeds mit Zend_Feed verarbeiten | 5-1 In diesem Fall wäre ein Zugriff auf author() ausreichend gewesen, da die Werte aller Elemente, die sich unterhalb von author befinden, automatisch mit ausgele- sen werden. Zum Vergleich hier noch einmal ein Ausschnitt aus den gekürzten XML-Daten: <?xml version="l.0" encoding="UTF-8"?> <feed version="0.3" xmlns="http://pur!.org/atom/ns#" xmlns:dc="http: //pur1.org/dc/elements/1.1/" xml:1ang="en"> <ti tle>admi nblogger,de</ti tle> <tagline>Geschichten aus dem Leben eines Linux-SysAdmins</tagline> <entry> <author> <name>Marcel</name> </author> <title type="text/html” mode="escaped"> Licht an (Aber richtig) </ti tle> <link rel="alternate" type=”text/html" href="http://www.admi nblogger.de/blog/2007/12/08/licht-an- aber-richtig/"/> <summary type="text/plain" mode=”escaped"> Anstatt zum Lichtabschalten aufzurufen, wie es Google... </summary> </entry> <entry> <author> <name>Marcel</name> </author> <title type="text/html” mode="escaped"> Neues Licht f&#252;rs Auto</title> <link rel=”alternate" type="text/htmr’ href="http://www.adminblogger.de/blog/2007/12/08/neues-licht- fuers-auto-osram-ni ghtbreaker-seat/"/> <summary type="text/plain" mode="escaped"> F&#252;r mein rollendes Gef&#228:hrt, ... </summary> </entry> </feed> Die Ausgabe des Scripts sehen Sie in Abbildung 5.2. 217
e-bol.net 5 | Webservices O http://127.0.0.1/zf-buch/feed/2.php CD uäöö http://127.0.0.1/zf-buch/feed/2.php - Q- Google Titel: adminblogger.de - Geschichten aus dem Leben eines Linux-SysAdmins A Anzahl der Einträge: 10 Licht an (Aber richtig) Autor Marcel Anstatt zum Lichtabschalten aufzurufen, wie es Google u.a. gerade tut, unterstütze ich lieber die Aktion Licht an. Die Hintergründe fasst Heise ganz gut zusammen. Neues Licht fürs Auto Autor Marcel Für mein rollendes Gefährt, was mich auch bei Dunkelheit sicher ans Ziel bringen soll, habe ich neue Halogenlampen gekauft. Angesteckt durch einen Arbeitskollegen, der bereits mit Philips X-tremc Power Halogcnlampcn gute Erfahrungen gemacht hat, habe ich mich für zwei OSRAM Nightbreakcr Halogcnlamper entschieden. Auf folgendem Bild sicht man inks die nach Herstellerangaben bis zu 90% hellere und [...1 Der Gewinner der US-Präsidentschaftswahlen 2008 steht fest Autor Marcel Der Gewinner der US-Präsidcntschaftswahlcn 2008 steht bereits fest. So zumindest umschreibt die Gruppe des HashClash-Projekts der Fakultät Mathematik und Informatik der technischen Universität Eindhoven das Experiment, bei der cs der Gruppe gelungen ist, mit Hilfe einer Playstation 3 zwölf unterschiedliche PDF-Dokumente zu generieren, die den gleichen MD5-Hash bilden. Alle 12 PDF-Dokumente enthalten jeweils den Namen eines [...1 Session-IDs in veröffentlichten URLs sind evil Autor Marcel Das zeigt das aktuelle Beispiel beim Shopblogger Eine Unachtsamkeit seitens des Bloggers Björn Harste der auf einen Artikel in seinem Shop vcrLnkcn wollte, aber vergaß, die Session-ID aus der URL zu entfernen. Jedem Besucher seines Blogs offenbarte er damit unfreiwillig (s?)einen Account, den auch scheinbar sofort Leute nutzten, um Artikel aus seinem Shop zu bestellen: Einmal im Shop [.„1 Operation Kingdom Abbildung 5.2 Ausgabe eines Atom-Feeds 5.1.5 Generieren von Feeds Das Schöne an Zend_Feed ist, dass Sie auf eine sehr einfache Art und Weise selbst Feeds generieren können, ohne dass Sie sich um XML oder die Struktur des Dokuments kümmern müssen. Die Daten, die im Feed enthalten sein sollen, übergeben Sie in Form eines asso- ziativen Arrays an die statische Methode importArray(). Als zweiten Parameter teilen Sie ihr dann noch mit, welches Format Ihr Feed haben soll. Das heißt, Sie können hier ’atom’ oder ’rss’ angeben, wobei ' atom’ der Default-Wert ist. Das Format des Arrays ist leider zu komplex, um es hier komplett zu erläutern. Daher werde ich nur die wichtigsten Elemente benutzen. Die komplette Struktur können Sie der Dokumentation unter der Adresse http://framework.zend.com/ manual/de/zend.feed.importing.html entnehmen. Dort finden Sie auch Informationen dazu, welche Array-Schlüssel genutzt wer- den müssen und welche optional sind. Bitte beachten Sie, dass einige Elemente bei einem Feed-Format benötigt werden und bei einem anderen nicht. Abhängig vom Format werden einige Elemente sogar ignoriert. 218
e-bol.net Feeds mit Zend_Feed verarbeiten | 5-1 In dem Array werden zunächst allgemeine Daten zum Feed deklariert, wie der Titel, die URL und Ähnliches. Die Beiträge selbst finden sich in einem indizierten Array, das unterhalb des Schlüssels entries zu finden ist. Jeder Beitrag muss über einige erforderliche Einträge verfügen. Das sind primär die Schlüssel title, link und descri pti on. Der erste deklariert den Titel des Beitrags, der zweite legt fest, unter welcher URL er aufgerufen werden kann, und mit descri ption kön- nen Sie einen kurzen Abriss des Inhalts geben. Wenn Sie das Array an die Klasse übergeben haben, erhalten Sie ein Objekt einer entsprechenden Zend_Feed-Klasse zurück. Rufen Sie sendO auf, werden die Daten in XML umgewandelt, mit den notwendigen Headern versehen und direkt an den Client gesendet. Möchten Sie die Daten lieber abspeichern oder ander- weitig nutzen, rufen Sie stattdessen saveXml () auf. Mit dieser Methode erhalten Sie die kompletten XML-Daten als String zurück. require_once ’Zend/Feed.php’; // Erster Eintrag Seintragl = array( 'title' => 'Erster Eintrag',// Titel 'link' => '127.0.0.1/blog/1',//URL zum kompletten Beitrag ’description’ => 'Mein erster kleiner Eintrag zum Testen' ); // Zweiter Eintrag Seintrag2 = array( 'title' => 'Zend_Feed ist toll', 'link' => '127.0.0.1/blog/2', 'description’=> 'Ja, Zend_Feed ist wirklich kinderleicht' ); $rss_daten =array( 'title' => 'Mein Blog', //Titel des Feeds 'link’ => ’127.0.0.1/blog’, // URL des Blogs ’charset' => 'UTF-8', // Zeichensatz 'entries' => array( // Array für die Beiträge Sei ntrag2, Sei ntragl ) ); try { // RSS-Feed-Objekt ableiten Sfeed = Zend_Feed::importArray(Srss_daten,'rss’); } 219
e-bol.net 5 | Webservices catch (Zend_Feed_Exception $e) I die ($e->getMessage()); I // Daten an Client senden $feed->send(); Die Ausgabe dieses Scripts sehen Sie in Abbildung 5.3 Abbildung 5.3 Selbst generierter Feed 5.2 Zugriff auf Amazon mit Zend_Service_Amazon Amazon ist sicher einer der bekanntesten Online-Händler. Wie viele andere große Internet-Plattformen bietet auch Amazon die Möglichkeit, mithilfe einer API (Application Programmers Interface) auf das Online-Angebot zuzugreifen. Damit haben Sie die Möglichkeit, Produkte von Amazon auf Ihrer eigenen Home- page anzubieten und das Angebot in Ihr Layout zu integrieren. Um Zugriff auf die Amazon-API zu haben, müssen Sie zunächst einen Account anlegen. Diesen können Sie unter http://aws.amazon.com beantragen. AWS ist übrigens die Abkürzung für Amazon Webservices. Unter AWS fasst der Anbieter alle Schnittstellen zusammen, die er zur Verfügung stellt. Nachdem Sie einen Account angelegt haben, erhalten Sie eine E-Mail. In dieser finden Sie einen Link, unter dem Sie Ihre »Access Key ID« herunterladen können. 220
e-bol.net Zugriff auf Amazon mit Zend_Service_Amazon | 5-2 Diesen Schlüssel benötigen Sie, damit Amazon Sie bei einem Zugriff auf die API identifizieren kann. Er hat allerdings nichts mit anderen Keys zu tun, die Amazon beispielsweise für Werbekostenprogramme nutzt. Er dient einzig und allein zur Authentifikation gegenüber der API. Nachdem Sie den Key erhalten haben, kön- nen Sie direkt auf die API zugreifen. Zend_Service_Amazon bietet momentan eine reine Abfragemöglichkeit an. Das heißt, Sie können bestimmte Produkte auslesen, nach Stichworten suchen und Rezensionen zu Produkten auslesen. Einige andere Klassen unterstützen nur den Zugriff auf die amerikanische Amazon-API. Zend_Servi ce_Amazon kann da mehr. Das Paket unterstützt den Zugriff auf die Amazon-Angebote der folgenden Län- der: Kanada (CA), Deutschland (DE), Frankreich (FR), Japan (JP), England (UK,) und USA (US). Die Countiy-Codes, die Sie nach den Namen der Länder jeweils in Klammern finden, stellen hierbei Strings dar, die Sie als zweiten Parameter an den Konstruktor übergeben. Der erste Parameter ist der Key, der obligatorisch übergeben werden muss. Der zweite Wert ist optional. Übergeben Sie keinen Countiy-Code, nutzt das Paket den Code US als Default-Wert. Im einfachsten Fall leiten Sie ein Objekt der Klasse ab und rufen dann die Methode i temSearch() auf, welche ein Array mit Optionen von Ihnen erwartet. Sie übergeben hier die Information, welche Stichworte Sie suchen und in wel- cher Kategorie Sie diese suchen. Den oder die Suchbegriffe übergeben Sie mit- hilfe des Schlüssels Keywords und die Kategorie mit dem Schlüssel Searchlndex. Im folgenden Beispiel wird Books genutzt, womit in der Kategorie »Bücher« gesucht wird. Dazu später aber mehr. requi re_once('Zend/Servi ce/Amazon.php'); // Daten von Amazon kommen als UTF-8 header('Content-Type: text/html; charset=utf-8'): // API-Key $key = ’W2V8EGGB0FH52S1AZ12’ ; // Amazon "Niederlassung" $land = ’DE': Samazon = new Zend_Service_Amazon($key,$land): //Suche nach php in Büchern Sparams = array(’Searchlndex’ => 'Books', 'Keywords' => 'php'); // Suche abschicken Sergebnisse = $amazon->itemSearch(Sparams): echo "Folgende B&uumljeher wurden gefunden:<br>"; foreach ($ergebnisse as $ergebnis) ( 221
e-bol.net 5 | Webservices echo "<p>"; echo "Ti tel: "; // Link zur Detailseite bei Amazon echo "<a href='$ergebnis->Detai1PageURL’>"; // Titel ausgeben echo $ergebnis->Title . '</a><br>*; // Autor(en) ausgeben i f (i s_array($ergebnis->Author)) { echo "Autoren:<br>"; foreach ($ergebnis->Author as Sautor) echo Sautor."<br>"; } el se { echo "Autor:<br>"; echo " .Sergebnis->Author; echo "</p>"; } Listing 5.4 Suchen von Büchern bei Amazon Die Ausgabe von Listing 5.4 finden Sie in Abbildung 5.4. Die Methode i temSearch() ist recht einfach zu nutzen. Aber gerade die Einfach- heit und Flexibilität, welche die Methode bietet, kompliziert das System schnell. Zuerst stellt sich die Frage, in welchen Kategorien Sie suchen können. Diese Kate- gorien unterscheiden sich von Land zu Land. In Deutschland können Sie zurzeit die folgenden Kategorien nutzen: Apparel, Baby, Books, Classical, DVD, Electronics, ForeignBooks, HealthPersonalGare, HomeGarden, Kitchen, Magazines, Music, OutdoorLiving, PCHardware, Photo, Software, Software- VideoGames, SportingGoods, Tools, Toys, VHS, Video, VideoGames und Wat- ches.1 Die Kategorienamen sind zum größten Teil selbsterklärend. Wenn Sie her- ausfinden wollen, welche Kategorien in dem jeweiligen Land verfügbar sind, finden Sie die Information in der Amazon-Dokumentation.1 2 Sollte Ihnen das zu aufwändig sein, so können Sie in der Abfrage auch einfach eine ungültige Kate- gorie nutzen. Dann wirft das System eine Exception, der Sie die gültigen Katego- rien entnehmen. 1 In absehbarer Zeit wird auch MusicTracks verfügbar sein. 2 http ://docs .amazonwebservices .com/AWSEcommerceService/2005-10-05/ 222
e-bol.net Zugriff auf Amazon mit Zend_Service_Amazon | 5-2 http://12 7.0.0. l/~carsten/zf-buch/Services/Amazon/2.php L± http://127.0.0. l/~carsten/zf-buch/Services/Amazon/2 * '(V Google QQ Planet PHP (113) Apple (7)v Amazon eBay Yahoo! Newsv my deldcio.us Folgende Bücher wurden gefunden: Titel: Joomla! Das Handbuch für Einstiger (Galileo Computing) ASIN: 3898426327 Verlag: Galileo Press Autoren: - Anja Ebersbach - Markus Glaser - Radovan Kubani Titel: Einstieg in PHP 5 und MySQL 5, Einführung in die Webprogrammiening (Galileo Computing) ASIN: 3898428540 Verlag: Galileo Press Autor Thomas Theis Titel: PHP und MySQL.Easy.. Dynamik für Ihre Webseiten ^Leicht, klar, sofort £M u. T easy) ASIN:3827269326 Vertag: Markt und Technik Autor Giesbcrt Damaschke Titel: Einstieg in TYPO3 4.0. Installation, Grundlagen, TypoScript (Galileo Computing) ASIN: 3898428362 Vertag: Galileo Press Autoren: - Andreas Stöckl - Frank Bongers Abbildung 5.4 Bei Amazon gefundene Bücher Zusätzlich können Sie auch noch Blended nutzen. In dem Fall wird in allen Kate- gorien gesucht. Dabei erhalten Sie allerdings oft mehr Ergebnisse als die zehn, die Sie standardmäßig erhalten. Die Angabe einer Kategorie muss erfolgen, um die Abfrage durchführen zu können. Neben dem Schlüssel Keywords können Sie noch eine große Anzahl von anderen Funktionalitäten nutzen. Allerdings handelt es sich dabei um eine so große Anzahl von Schlüsseln, dass ich sie hier nicht komplett erläutern kann. Die aus meiner Sicht wichtigsten Schlüssel finden Sie in Tabelle 5.1. Schlüssel Beschreibung Aval 1 abi 1 i ty Legt fest, ob nur Artikel gefunden werden sollen, die auch verfügbar sind. Möglicher Wert ist Aval 1 abl e. Bitte beachten Sie, dass Sie bei der Nut- zung dieses Schlüssels Conditi on auf Al 1 setzen müssen. Merchantld sollte auf Al 1 gesetzt sein. Searchlndex Kategorie, in der gesucht werden soll (s. o.) Tabelle 5.1 Schlüssel für die Suche bei Amazon 223
e-bol.net 5 | Webservices Schlüssel Beschreibung Keywords Schlüsselworte, nach denen gesucht werden soll, werden in Form eines Strings angegeben. Title Begriffe, die Sie mit diesem Schlüssel übergeben, werden nur im Titel der Produkte gesucht. Artist Künstler, der das Werk veröffentlicht hat. Zulässig bei CI assi cal und Music. Author Autor des Werks; zulässig bei Forei gnBooks und Books Actor Schauspieler, der an dem Film mitgewirkt hat. Kann bei VHS, Video und DVD genutzt werden. Di rector Regisseur des Films. Kann in den Kategorien VHSr Video und DVD genutzt werden. Composer Komponist eines Musikstückes, kann in der Kategorie CI assi cal genutzt werden. Publi sher Unternehmen oder Verlag, das/der ein Produkt herausgibt oder veröffent- licht. Kann in den Rubriken VHS, Video, Books, DVD, ForeignBooks und Magazi nes genutzt werden. ItemPage i temSearch() liefert pro Suche 10 Treffer; werden mehr Ergebnisse gefunden, können Sie mit ItemSearch eine Seite (-10 Ergebnisse) ange- ben, die abgerufen werden soll. Die erste Seite hat die Nummer 1. MinimumPrice Minimaler Preis des Produkts. Der Preis muss in Cent angegeben werden, also z. B. 500 für 5 Euro. MaximumPri ce Maximaler Preis des Produkts. Der Preis muss in Cent angegeben werden, also z. B. 650 für 6,50 Euro. Merchant Id Hiermit können Sie definieren, bei welchen Anbietern die Angebote durchsucht werden sollen. Übergeben Sie Al 1, werden alle durchsucht, bei dem Wert Featured werden nur Anbieter durchsucht, deren Angebot in den Einkaufswagen gelegt wird, wenn Sie den »In den Einkaufswagen«- Button anklicken. Alternativ können Sie auch die ID eines einzelnen Anbieters übergeben. Conditi on Zustand des gesuchten Artikels. Mögliche Werte sind Al 1, New, Used, Refurbi shed und Col1ecti ble. ResponseGroup Definiert, welche Daten Sie als Antwort erwarten. Tabelle 5.1 Schlüssel für die Suche bei Amazon (Forts.) Wie schon gesagt, ist die Liste aus Tabelle 5.1 nicht komplett. Eine vollständige Liste finden Sie unter dem Menüpunkt »ItemSearch« auf der Website http://docs.amazonwebservices.com/AWSEcommerceService/2005-W-05/. Hier finden Sie unter dem Menüpunkt »Searchlndex/Parameter Combinations« auch die Information, bei welchen Suchanfragen welche Parameter mit welchen anderen kombiniert werden dürfen bzw. müssen. 224
e-bol.net Zugriff auf Amazon mit Zend_Service_Amazon | 5-2 Zwar ist itemSearch( )sehr flexibel, aber auch sehr komplex. Möchten Sie also eine Suchfunktion damit umsetzen, sollten Sie sich vorher gut mit den Möglich- keiten und Anforderungen vertraut machen, um keine Exceptions zu erhalten.3 Nun ist noch zu klären, was die Methode zurückgibt. Auch dieses Problem ist nicht ganz trivial, wie Sie gleich sehen werden. Bei einem direkten Rückgabewert handelt es sich um ein Objekt der Klasse Zend_Service_Amazon_Resul tSet. Da diese einen Iterator implementiert, können Sie das Objekt direkt an eine for- each-Schleife übergeben, die bei jedem Durchlauf ein Objekt der Klasse Zend_ Service_Amazon_Item erhält. In diesem Objekt sind die einzelnen Werte in Eigenschaften abgelegt. Standardmäßig sind in der Antwort für jedes Produkt mindestens die Eigenschaften ASIN und Detai 1 PageURL enthalten. Die erste ent- hält die Amazon-eigene Artikelnummer und die zweite einen Link auf die Detail- seite des Produkts. Die anderen Eigenschaften sind ein wenig schwieriger zu handhaben. Das hat zwei Gründe: Erstens nutzt Amazon die Eigenschaften nicht ganz konsistent. Der zweite Grund ist, dass die Eigenschaften auch teilweise ein- fach leer sein können. Das heißt, die Eigenschaft im Objekt wird mit NULL belegt. Darüber hinaus kann es vorkommen, dass eine Eigenschaft mehrere Werte bein- haltet, die nicht nur als String, sondern als Array übergeben werden. In dem nachfolgenden Beispiel soll nach Büchern gesucht werden, wobei nach den Begriffen PHP und MySQL gesucht werden soll: requi re_once('Zend/Servi ce/Amazon.php'); // Daten von Amazon kommen als UTF-8 header('Content-Type: text/html; charset=utf-8'): // API-Key $key = ’02V8DF43B0TF2S1AZ02’; // Amazon "Niederlassung" $land = •DE'; Samazon = new Zend_Service_Amazon($key,$land); //Suche nach php in Buechern Sparams = array(’Searchlndex’ => ’Books’, 'Keywords'=>'PHP, Mysql'): // Suche abschicken Sergebnisse = $amazon->itemSearch(Sparams): echo "Folgende B&uumljeher wurden gefunden:<br>"; foreach ($ergebnisse as $ergebnis) ( 3 Momentan wirft die Methode eine Exception, wenn keine Ergebnisse gefunden wurden. Das soll sich allerdings in Version 1.1.0 ändern. 225
e-bol.net 5 | Webservices // Handelt es sich wirklich um ein Buch? if ('Book' != Sergebnis->ProductGroup) { conti nue; echo "<p>": echo "Titel: "; // Link zur Detailseite bei Amazon echo "<a href=*$ergebnis->Detai1PageURL’>"; // Titel ausgeben echo $ergebnis->Title . '</a><br>’; // ASIN ausgeben echo "ASIN: ".$ergebnis->ASIN .'<br>'; // Verlag ausgeben echo "Verlag: if (!empty(Sergebnis->Manufacturer)) { echo Sergebnis->Manufacturer; el se { echo "Unbekannt": echo "<br>"; // Autor(en) ausgeben i f (1s_array($ergebnis->Author)) { echo "Autoren:<br>”; foreach ($ergebnis->Author as Sautor) echo ".Sautor."<br>"; } el se { echo "Autor: "; if (!empty(Sergebnis->Author)) echo Sergebnis->Author; 1 el se echo "Unbekannt": ) 226
e-bol.net Zugriff auf Amazon mit Zend_Service_Amazon | 5-2 echo "</p>"; } Listing 5.5 Komplexere Buch-Suche Innerhalb der foreach-Schleife werden die Daten ausgegeben. Die Eigenschaften erläutere ich sofort. Zuvor lassen Sie mich aber noch auf die folgende i f-Abfrage eingehen: if ('Book' != tergebnis->ProductGroup) { conti nue: } Sie sollte eigentlich nicht nötig sein, ist aber hilfreich. In einigen Fällen liefert Amazon Datensätze von Produkten zurück, die man nicht erwartet hatte. Daher testet diese Abfrage, ob in der Eigenschaft ProductGroup der String Book enthal- ten ist. Sollte das nicht der Fall sein, macht die Schleife mit der nächsten Iteration weiter. Welche Produktgruppen es gibt, entnehmen Sie bitte der Amazon-Doku- mentation. Innerhalb der Schleife wird zunächst ein Link auf die Detailseite des Produkts generiert. Der Link setzt sich aus dem eigentlichen Link-Ziel, das in der Eigen- schaft Detai 1 PageURL enthalten ist und dem Link-Text zusammen. Als Link-Text, also das, was der Benutzer anklicken kann, wird hier der Titel des Buches ver- wendet, der in der Eigenschaft Title abgelegt ist. Danach wird die ASIN ausgegeben. Bei der ASIN, der Amazon Standard Identifi- cation Number, handelt es sich um eine Nummer, über die jedes Produkt bei Amazon eindeutig referenziert werden kann. Diese ist sehr hilfreich, wenn Sie eine Seite mit Detailinformationen zu den Produkten erstellen möchten. Die Ausgabe des Verlags ist mit einer i f-Abfrage verknüpft. Leider sind im Sys- tem von Amazon nicht immer alle Daten gepflegt. Zwar wird jedes Buch aus einem Verlag kommen, aber es kann sein, dass dieser einfach nicht im Amazon- System bekannt ist. Der Verlag wird unter anderem in der Eigenschaft Manufac- turer geführt. Interessant ist wieder das Auslesen des Autors bzw. der Autoren. Hier kann es vorkommen, dass nicht nur ein Wert enthalten ist. Ist nur ein Autor vorhanden, enthält die Eigenschaft einen String. Sollte es mehrere Autoren geben, so ist hier ein Array enthalten. Das gilt auch für alle anderen Eigenschaften. Auch wenn es nicht sehr wahrscheinlich ist, könnte es durchaus sein, dass mehrere Verlage ein- getragen sind oder Ähnliches. Sie sollten daher bei einer produktiven Anwen- dung bei jeder Eigenschaft prüfen, ob sie ein Array enthält. 227
e-bol.net 5 | Webservices Sie sehen, die Suche nach bestimmten Produkten ist nicht sonderlich schwierig. Bei genauer Betrachtung ist die Auswertung der gelieferten Ergebnisse allerdings nicht so ganz einfach. Neben den schon angesprochenen Punkten stellt sich immer die Frage, welche Elemente überhaupt im Ergebnis enthalten sein kön- nen. Das hängt von mehreren Faktoren ab. Da wäre zuerst der Punkt, nach wel- cher Art von Produkten Sie gesucht haben, bzw. in welcher Kategorie Sie gesucht haben. Zum zweiten stellt sich die Frage, welche Response-Group Sie ausgewählt haben. Durch die Definition der Response-Group teilen Sie Amazon mit, welche Daten in der Antwort enthalten sein sollen. Standardmäßig nutzt das Paket die Response-Group Smal 1. Möchten Sie eine andere Gruppe nutzen, so können Sie diese auch über das Array definieren, welches Sie an die Methode i temSearch() übergeben. Mit dem dafür zuständigen Schlüssel ResponseGroup dürfen ein oder mehrere Antwort-Schemata übergeben werden. Möchten Sie mehrere Schemata übergeben, so fassen Sie diese einfach in einem String zusammen, wobei die ein- zelnen Werte jeweils durch ein Komma zu trennen sind. Bitte beachten Sie, dass hier wirklich nur ein Komma zwischen den einzelnen Gruppen stehen darf. Soll- ten Sie zusätzlich ein Leerzeichen oder einen anderen Whitespace nutzen, führt das (momentan noch) zu einer Exception. Alternativ können Sie die Werte auch als Array übergeben. Die folgenden Antwort-Gruppen sind hierbei zulässig: Request, Itemlds, Smal1 , Medi um, Large, OfferFul1 , Öfters , OfferSummary, Vari ations , Vari - ationMinimum, VariationSummary, ItemAttributes, Tracks, Accessories, EditorialReview, SalesRank, BrowseNodes, Images, Simi1arities, Reviews, ListmaniaLists, SearchBins, Subjects. Die Gruppen liefern jeweils einen unterschiedlichen Umfang an Daten zurück und sorgen dafür, dass nicht die eigentlichen Produkte, sondern beispielsweise die dazugehörigen Bilder (Images) oder Kundenkommentare (Reviews) ausgelesen werden. Da es den Rahmen des Buches sprengen würde, die einzelnen Response-Gruppen zu besprechen, muss ich Sie an dieser Stelle leider wieder auf die Dokumentation verweisen. Allerdings sei mir der Hinweis gestattet, dass eine Gruppe unter Umständen andere Gruppen mit einschließen kann. So umfasst die Gruppe Large beispiels- weise auch die Inhalte von Small , Request, ItemAttributes, OfferSummary, SalesRank, Editorial Review, Images, Tracks, BrowseNodes, Reviews, List- maniaLi sts , Simi1arities, Öfters und Accessori es. Im folgenden Beispiel sollen Informationen zum Buch »eZ Components« gesucht werden. Um keine Stichwortsuche durchführen zu müssen, wird in diesem Bei- spiel nicht i temSearch(), sondern i temLookup() genutzt. Die Methode sucht ein bestimmtes Produkt heraus und bekommt als ersten Parameter die ASIN des Arti- kels übergeben. Als zweiten Parameter können Sie der Methode ein Array mit 228
e-bol.net Zugriff auf Amazon mit Zend_Service_Amazon | 5-2 Optionen übergeben, wobei die hier akzeptierten Parameter identisch mit denen von 1 temSea rch() sind. Hier ist es allerdings nicht sinnvoll, Stichworte oder eine Kategorie zu übergeben, da das Produkt direkt selektiert wird. Als Response- Group wird in diesem Fall Large genutzt. Da itemLookup() exakt einen Treffer liefert, müssen die Daten auch nicht in einer Schleife verarbeitet werden. Sollte zu der ASIN, die Sie übergeben haben, kein Treffer gefunden werden, resultiert das zurzeit noch in einer Exception. Wie Sie sehen werden, enthält das Ergebnis-Objekt verschiedene andere Objekte. Für Bilder, Kundenrezensionen und Ähnliches sind innerhalb des Pakets eigene Klassen vorgesehen. Welche Klassen definiert und welche Eigen- schaften deklariert sind, können Sie in der Dokumentation unter http://framework. zend.com/manual/de/zend.service.amazon.html nachlesen. requi re_once('Zend_reposi tory/Service/Amazon.php'); // Daten von Amazon kommen als UTF-8 header('Content-Type: text/html; charset=utf-8'); // API-Key Skey = ’04VEP4GCS1AZ0AF' ; // Amazon "Niederlassung" $1 and = ’DE'; Samazon = new Zend_Service_Amazon(Skey,$land); //Suche nach ASIN 3836210738 Sparams = array(’ResponseGroup' => 'Large'); // Suche abschicken Sergebnis = $amazon->itemLookup('3836210738',$params); // Bild ausgeben // Neben Mediumimage gibt es auch noch Largelmage und Small Image $bild_url = Sergebnis->MediumImage->Url; $bild_breite = Sergebnis->MediumImage->Width; $bild_hoehe = Sergebnis->MediumImage->Height; echo"<tableXtrXtd>"; echo "<img src=’Sbi1d_ur1’ width='$bi1d_breite' height='$bild_hoehe’>"; echo "</td><td>"; echo "Titel: "; // Link zur Detailseite bei Amazon echo "<a href='Sergebnis->Detai1PageURL'>"; // Titel ausgeben echo Sergebnis->Title . ’</aXbr>'; 229
e-bol.net 5 | Webservices // Verlag ausgeben echo "Verlag: ".Sergebnis->Manufacturer; echo "<br>"; // Autoren ausgeben echo "Autoren:<br>"; foreach (Sergebnis->Author as $autor) { echo .Sautor. "<br>"; } // Seitenzahl echo "Seiten: ".Sergebnis->NumberOfPages: echo "<br>": // ISBN-Nummer ausgeben echo "ISBN-Nummer: Sergebnis->ISBN; echo "<br>": // EAN-Nummer ausgeben echo "EAN-Nummer: Sergebnis->EAN; echo "<br>": // Kundenbewertung echo "Durchschnittliche Kundenbewertung: echo Sergebnis->AverageRating; echo " Sterne <br>"; // Anzahl der Kundenrezesionen echo "Anzahl der Kundenbewertungen: echo Sergebnis->TotalReviews: echo "</tdX/trX/table>": // Guenstigsten Anbieter auslesen: Sanbieter = Sergebnis->Offers: // Preis ausgeben echo "Preis: // Achtung! Preis ist ein Integer-Wert printf (’%.2f',Sanbieter->LowestNewPrice/100): echo " ".Sanbieter-SLowestNewPriceCurrency: // Kundenrezensionen ausgeben: echo "<hrXbrXb>Kundenrezensionen</b>"; Srezensionen = Sergebnis->CustomerReviews: foreach (Srezensionen as Srezension) { echo "<p><b>Srezension->Summary</b><br>"; echo ”$rezension->Content</p>": } Listing 5.6 Ausgabe von Details zu einem Buch 230
e-bol.net Zugriff auf Amazon mit Zend_Service_Amazon | 5-2 http://12 7.0.0.1/-carsten/zf-buch/Services/Amazon/3.php < ► : + | 0 C * 'Q’ Google OQ Planet PHP (113) Apple (7)▼ Amazon eBay Yahoo! News^ my del.ido.us Titel: eZ Components Verlag: Galileo Press Autoren: - Tobias Schlitt - Kore Nordmann Seiten: 454 ISBN-Nummcr. 3836210738 EAN-Nummer. 9783836210737 Durchschnittliche Kundenbewertung: 5 Sterne Anzahl der Kundcnbcwcrtungcn: 1 Preis: 39.90 EUR Kundenrezensionen Tobias Schlitt und Kore Nordmann stammen aus dem Entwicklerteam und bieten mit ihrem Buch einen umfassenden Einstieg in eZ Compo Tobias Schlitt und Kore Nordmann stammen aus dem Entwicklcrteam und bieten mit ihrem Buch einen umfassenden Einstieg in eZ Components. Auch komplexe PHP-Applikationen lassen sich so einfach und schnell mit der Klasscnbibliothck erstellen. Mit eZ Components erreicht zum ersten Mal eine professionelle Komponcnten-Bibliothek die PHP- Gemeinde. Die hochqualitativen PHP-5-Bausteine lassen sich in beliebige Anwendungen und jedes Framework integrieren. Neben Komponenten zur Datenbankabstraktion sind unter anderem auch eine modulare Template-Engine, verschiedene Pakete zur Grafikbearbeitung, Daten Visualisierung (Charts) oder zur Realisierung von Plug-In-Architekturcn (SignalSlot) im Angebot. Insgesamt stellt eZ Components zurzeit 26 Komponenten und 7 Tie-In-Komponcntcn zur Verfügung. Abbildung 5-5 Darstellung von Details zu einem Buch Die meisten Punkt in diesem Listing sind sicher selbsterklärend, sodass ich nur auf die interessanten Punkte eingehen werde. Jedes Produkt verfügt über drei verschieden große Bilder. In diesem Beispiel wurde das Bild-Objekt genutzt, das in der Eigenschaft Mediumimage abgelegt ist. Zusätzlich existieren noch die Eigenschaften Small Image und Largelmage, die Informationen zu dem kleinen und dem großen Bild enthalten. Beide können identisch mit Medi um Image genutzt werden. Der günstigste Anbieter für das Produkt wird in diesem Fall dadurch ermittelt, dass das Objekt Offers ausgelesen wird. In den hier genutzten Eigenschaften sind die Informationen über den günstigsten Anbieter direkt enthalten. Darüber hinaus ist in dem Objekt noch ein Array namens Offers enthalten, in dem Sie Detailinformationen zu dem günstigsten Anbieter bzw. noch weitere Anbieter finden. Bitte beachten Sie bei den Preisen, dass Amazon diese immer als Integer- Werte, also als Cent-Beträge, ausliefert. Bei der Arbeit mit der Amazon-API stellen Sie schnell fest, dass sie technisch zwar gut, aber nicht unbedingt einfach zu benutzen ist. Das hauptsächliche Problem besteht darin, dass die Eigenschaften nicht immer konsistent genutzt werden. 231
e-bol.net 5 | Webservices Des Weiteren beachten Sie bitte, dass Zend_Service_Amazon bisher noch einige Features vermissen lässt. So können Sie beispielsweise noch nicht herausfinden, wie der Name eines Rezensenten lautet. Prüfen Sie vor dem Einsatz also, ob das Paket Ihre Anforderungen abdeckt. 5.3 Zugriff auf Flickr mit Zend_Service_Flickr Flickr ist zurzeit eine der größten, wenn nicht sogar die größte, Foto-Communi- ties im Internet. Viele Hobby- und Profifotografen stellen hier ihre Bilder der Öffentlichkeit vor. Die Bilder können von den Eigentümern dabei nach bestimm- ten Kriterien verschlagwortet und sortiert werden. Zend_Servi ce_Fl i ckr gibt Ihnen die Möglichkeit, die öffentlichen Bilder der Benutzer nach bestimmten Kriterien zu suchen. Sie können nach bestimmten Schlagworten oder nach Bildern eines Benutzers suchen. Weitere Funktionalitä- ten, die Flickr unterstützt, werden momentan durch die Klasse nicht abgedeckt. Bevor Sie loslegen können, benötigen Sie allerdings auch hier einen API-Schlüs- sel. Diesen erhalten Sie, indem Sie einen Account bei Flickr anlegen. Alle not- wendigen Informationen finden Sie unter der Adresse http://www.ßickr.com/ services/api/. Nachdem Sie ein Objekt der Klasse abgeleitet haben, wobei Sie den API-Schlüssel an den Konstruktor übergeben, können Sie mit den Methoden tagSearch() und userSearch() auf Bildersuche gehen. Mit tagSearch() können Sie nach Bildern zu bestimmten Schlagworten suchen, wohingegen userSearch() die Bilder eines bestimmten Anwenders liefert. Die Schlagworte, zu denen Sie Bilder suchen, können Sie tagSearch() als ersten Parameter in Form eines Arrays oder als String übergeben, wobei die Suchbe- griffe jeweils durch ein Komma zu trennen sind. userSearch() akzeptiert die E-Mail-Adresse eines Benutzers oder den Benutzer- namen als Parameter. Beide Methoden akzeptieren als zweiten Parameter ein Array, mit welchem Sie das Verhalten der jeweiligen Methode beeinflussen. Sie können damit steuern, wie viele Treffer auf einmal zurückgegeben und welche zusätzlichen Informatio- nen ermittelt werden sollen. Die wichtigsten Schlüssel in diesem Zusammenhang sind sicher per_page und page. Mit dem ersten Schlüssel definieren Sie, wie viele Treffer pro Anfrage zurückgegeben werden. Der Default-Wert, den die Klasse nutzt, ist 10. Maximal können Sie hier 500 angeben. Mit page teilen Sie Flickr mit, auf welcher Seite Sie sich gerade befinden, wenn mehr Treffer gefunden 232
e-bol.net Zugriff auf Flickr mit Zend_Service_Flickr | 5-3 wurden, als auf einer Seite dargestellt werden können. Die Gesamtzahl der mög- lichen Schlüssel ist leider zu groß, um sie hier komplett zu erläutern. Sie können sie der Flickr-Dokumentation entnehmen. Die möglichen Schlüssel bei tag- Search() finden Sie unter der Adresse http://www.flickr.com/services/api/flickr. people.getPublicPhotos.html. Und unter http://www.flickr.com/services/api/flickr. photos.search.html finden Sie die Möglichkeiten, die tagSearchf) Ihnen bietet. Die Methoden liefern als Rückgabewert jeweils ein Objekt vom Typ Zend_ Service_Fl ickr_ResultSet. Diese Klasse implementiert das Interface Seek- abl elterator aus der SPL von PHP.4 Dadurch lässt sich das Objekt auch direkt in einer foreach-Schleife nutzen. Wichtige Eigenschaften in diesem Objekt sind total Resul tsAvai 1 abl e (Gesamt- zahl der Treffer), total Resul tsReturned (Anzahl der zurückgelieferten Treffer) sowie fi rstResultPosition (Position des ersten gelieferten Treffers in der Gesamtliste). Bei jedem einzelnen Treffer, den Sie mit einer Schleife oder durch direkten Zugriff auf eine der Interface-Methoden auslesen, handelt es sich um ein Objekt der Klasse Zend_Servi ce_Fl 1 ckr_Resul t. In ihm sind die wichtigsten Informati- onen zu einem Bild enthalten. Die wichtigsten Eigenschaften dieser Klasse finden Sie in Tabelle 5.2.5 Eigenschaft Wert 1 d eindeutige ID des Bildes title Titel des Bildes ownername Benutzername des Eigentümers dateupload Datum, zu dem das Bild zu Flickr hochgeladen wurde (UNIX-Timestamp) datetaken Datum, an dem das Bild aufgenommen wurde (MySQL-Datetime-Format) Tabelle 5.2 Eigenschaften des Flickr-Result-Objekts In den vorgenannten Eigenschaften sind also nur Meta-Informationen zu dem Bild zu finden. Der Hintergrund ist, dass Flickr zu einem Bild immer gleich Kopien in verschiedenen Größen anlegt. Informationen zu diesen einzelnen Bil- dern sind in Form von Zend_Service_Fl ickr_Image-Objekten in den Eigen- schaften aus Tabelle 5.3 abgelegt. 4 Eine Dokumentation der SPL finden Sie unter http://www.php.net/~hell_y/php/ext/spl/. 5 Eine komplette Liste der Eigenschaften finden Sie hier: http://framework.zend.com/manual/ en/zend.Service.flickr.html#zend.Service.flickr.classes.result 233
e-bol.net 5 | Webservices Eigenschaft Bildinformation Square Ein 75 x 75 Pixel großes Thumbnail des Bildes Thumbnai1 Ein 100 Pixel großes Thumbnail Smal 1 Ein 240 Pixel großes Thumbnail Medi um Eine Variante mit 500 Pixeln Large Eine Variante mit 1024 Pixeln Original Das Originalbild Tabelle 5.3 Eigenschaften des Image-Objekts Falls Sie sich gerade wundern sollten, warum die Größenangaben in Tabelle 5.3 mit z. B. 500 Pixel ein wenig ungewöhnlich sind, so gibt es dafür eine einfache Erklärung. Bei Hochformaten handelt es sich dabei um die Höhe, bei Querforma- ten hingegen um die Breite. Für jede dieser Eigenschaften ist jeweils ein Objekt der Klasse Zend_Servi ce_ Flickr_Image hinterlegt oder NULL, falls ein Bild in der entsprechenden Größe nicht vorhanden sein sollte. Die einzelnen Bild-Objekte enthalten noch die Eigenschaften height und width mit der Höhen- und Breitenangabe sowie uri und clickUri. Die Angabe uri enthält dabei die URI des eigentlichen Bildes, wohingegen cl i ckUri auf eine Seite bei Flickr verweist, auf der das Bild betrach- tet werden kann. < ? php requi re_once 'Zend/Servi ce/Flickr.php'; try { $flickr = new Zend_Service_Flickr('758eb981d3a88b3121f77 '); // Zwei Bilder pro Seite $opts = array(’per_page’=>2); // Wir wollen nur die Bilder von Benutzer thermoman Streffer = $flickr->userSearch("thermoman",$opts): // Wurden Bilder zurückgegeben? if (0 < $treffer->totalResultsReturned) // Bilder in Tabelle ausgeben ausgeben echo ’'<table>"; foreach ($treffer as $bild) { // Pro Bild Thumbnail und Titel echo "<tr>"; echo "<td><img src=’{$bild->Smal1->uri}'></td>"; 234
e-bol.net Zugriff auf Flickr mit Zend_Service_Flickr | 5-3 echo "<td>Titel: $ bi 1d->1111e<br> Flickr-User: $bild->ownername<br> Aufgenommen: $bi1d->datetaken<br> <a href-'{$biId->Large->clickUri} ’> Bild bei Flickr betrachen</a> </td>"; echo "</tr>"; 1 echo ”</table>"; 1 el se echo "Keine Bilder gefunden!"; } } catch (Zend_Service_Exception $e) { die ($e->getMessage()): 1 Listing 5.7 Suche nach Bildern eines Benutzers bei Flickr Die Ausgabe von Listing 5.7 sehen Sie in Abbildung 5.6. Natürlich ist das Script noch nicht perfekt. So fehlt beispielsweise noch eine Blätterfunktion. http://carsten-mohrkes-com...en/zf-buch/flickr/eins.php CD QQOO O http://ca'sten-m< -fcb Google OCamino Info u ?News Mac News Tabs [Q|Cooqle । ]Paypal Titel: wind power Flickr-User thcrmoman Aufgenommen: 2007-09-08 11:03:42 Bild bei Flickr betrachen Titel: katrin Flickr-User thcrmoman Aufgcnommen: 2007-05-19 09:59:26 Bild bei Flickr betrachen Abbildung 5.6 Bilder des Benutzers »thermoman« 235
e-bol.net 5 | Webservices Weitere Funktionalitäten sind in der Klasse momentan leider noch nicht vorgese- hen. Es bleibt zu hoffen, dass diese noch ergänzt werden, da Flickr eine wirklich mächtige API anbietet. 5.4 Yahool-Suche mit Zend_Service_Yahoo Auch Yahoo! als einer der größten Anbieter von Dienstleistungen im Internet bietet eine API an, über die die Dienste angesprochen werden können. Damit Sie sich gegenüber der API authentifizieren können, benötigen Sie auch hier einen Schlüssel. Diesen erhalten Sie, wenn Sie auf der Seite http://developer.yahoo.com auf den Link »Get an Application ID« klicken. Zend_Service_Yahoo unterstützt den Zugriff auf verschiedene Dienste von Yahoo!. Neben der altbekannten Websuche ist es auch möglich, nach Bildern, Nachrichten oder lokalen Adressen zu suchen. Um an dieser Stelle schon eine Sache vorwegzunehmen: Alle Suchen liefern eine Ergebnisobjekt zurück, welches das SPL-Interface Seekabl elterator implemen- tiert. Sie können das Ergebnisobjekt somit direkt in einer foreach-Schleife nut- zen. Oder Sie können mithilfe der Methoden auf die Inhalte zugreifen, die das SPL deklariert. Eine Erläuterung der Methoden finden Sie unter http://www. php. net/~helly/php/ext/spl/. Die einzelnen Suchabfragen werden jeweils mit einem Zend_Service_Yahoo- Objekt ausgeführt. Es gibt also keine spezialisierten Klassen für die Abfragen, sondern lediglich spezielle Methoden. Der API-Schlüssel wird hierbei direkt an den Konstruktor übergeben. 5.4.1 Websuche Die Websuche bei Yahoo! erfolgt mithilfe der Methode webSearch(). Der Such- string wird der Methode dabei direkt als Parameter übergeben. Über einen zwei- ten, optionalen Parameter können Sie noch das Suchverhalten beeinflussen. An dieser Stelle können Sie ein assoziatives Array übergeben. Die gültigen Schlüssel finden Sie in Tabelle 5.4. 236
e-bol.net Yahoo!-Suche mit Zend_Service_Yahoo 5-4 Schlüssel Bedeutung results Anzahl der Treffer, die maximal zurückgeliefert werden sollen (Default: 10, Maximum 100) start Offset des ersten Treffers, der zurückgegeben werden soll. Die Zählung der Treffer beginnt mit 1. 1anguage ISO-Sprachkürzel der Sprache, in der gesucht werden soll. Es kann immer nur in einer Sprache gesucht werden; die Default-Sprache ist Englisch.6 type Bestimmt, wie die Suchabfrage verarbeitet wird. Zulässig sind die Werte any, al 1 und phrase. Bei any muss mindestens einer der Suchbegriffe auf der Seite zu finden sein, al 1 legt fest, dass alle Wörter zu finden sein müs- sen, und phrase definiert den Suchbegriff als feststehende Phrase. si te Hiermit können Sie einschränken, auf welcher Website gesucht werden soll. Übergeben Sie hier www.exampl e . com, wird nur auf diesem Server gesucht. Mehrere Sites - bis zu 30 sind möglich - können mit & verknüpft werden. format Dateiformat, in dem der Treffer vorliegen soll. Möglich sind die Werte any, html, msword, pdf, ppt, rss, txt und xl s. Der Default-Wert any steht für beliebige Datentypen. adult_ok Hiermit können Sie einen booleschen Wert übergeben, der definiert, ob die Suche auch möglicherweise jugendgefährdende Inhalte liefern darf, was standardmäßig nicht der Fall ist. similar_ok Übergeben Sie hier true, so werden auch Seiten mit identischem Inhalt und unterschiedlichen URLs zurückgegeben, was standardmäßig nicht so ist. country Mit diesem Schlüssel geben Sie einen Länder-Code an, der festlegt, dass nur Treffer aus diesem Land geliefert werden sollen.7 11cense Möchten Sie nur Treffer geliefert bekommen, die einer bestimmten Lizenz unterliegen, so können Sie diese hier angeben. Der Default-Wert any lie- fert Inhalte mit jeder beliebigen Lizenz. Außerdem werden die folgenden Creative Commons Lizenzen unterstützt: cc_any, cc_commerci al, und cc_modi fi abl e Tabelle 5.4 Optionen für die Websuche Die Methode webSearchf) gibt ein Objekt von Typ Zend_Servi ce_Yahoo_WebRe- sultSet zurück. Bei diesem Objekt ist die Eigenschaft totalResultsAvai1able recht hilfreich. Sie enthält die Gesamtzahl der Treffer, die Yahoo! zu der Abfrage ermitteln konnte. Die Anzahl der Treffer, die geliefert wurden, können Sie der Eigen- schaft total Resul tsReturned entnehmen. Ebenso wird auch der Offset des ersten Treffers zurückgegeben; er befindet sich in der Eigenschaft f i rstResul tPosi ti on. 6 Bitte beachten Sie, dass nur bestimmte Sprachen unterstützt werden. Die Auswahl einer nicht zulässigen Sprache resultiert in einer Exception. Die zulässigen Sprach-Codes finden Sie hier: http://developer.yahoo.com/search/languages.html 7 Die Liste der unterstützten Länder finden Sie unter http://developer.yahoo.com/search/ countries.html. 237
e-bol.net 5 | Webservices Die einzelnen Treffer der Suche können mit einer foreach-Schleife ausgelesen werden. Jeder Treffer liegt in Form eines Zend_Service_Yahoo_WebResult- Objekts vor. Die Eigenschaften des Objekts entnehmen Sie bitte Tabelle 5.5. Eigenschaft Erläuterung Title Titel der Webseite, die gefunden wurde Uri URL des gefunden Treffers (zur Bildschirmdarstellung) CIickUrl URL, die zum Verlinken genutzt werden soll. In dem Fall protokolliert Yahoo! den Klick und leitet auf die Zielseite um. Summary Kurze Zusammenfassung der Seite oder Ausschnitt aus der Seite MimeType MIM E-Type des gefundenen Dokuments Modi f1cati onDate Zeitpunkt der letzen Veränderung als UNIX-Timestamp CacheUrl URL der Seite im Yahoo!-Cache CacheSize Größe der Datei im Cache Tabelle 5.5 Eigenschaften des Result-Objekts < ? php requi re_once 'Zend/Service/Yahoo.php'; header('Content-Type: text/html; charset=utf-8'); try { $yahoo = new Zend_Service_Yahoo("vxH86JA6B76D0TZqNSXg--"); $opts = array('1anguage’=>'de', ’start'=>2); $results = $yahoo->webSearch(utf8_encode('Galileo'),$opts); if (0 < $results->totalResultsReturned) echo *<table>*; foreach ($results as $result) { echo "<tr>"; echo "<td colspan='2'>$result->Title</td>"; echo "</tr>"; echo "<tr>"; echo "<td >&nbsp;</tdXtd>$result->Summary</td>"; echo "</tr>"; echo "<tr>"; echo ”<td>&nbsp:</td>": echo ”<td>Zur Seite: <a href='$result->Clickl)rl ’> $ res ult->Url</a></td>"; 238
e-bol.net Yahool-Suche mit Zend_Service_Yahoo | 5-4 echo "</tr>"; echo echo "<td>&nbsp;</td>": echo "CtdXa href=’$result->CacheUrl’> gecachte Seite $result->CacheSize</a></td>"; echo "</tr>"; echo "<tr>"; echo "<td colspan=’2’>&nbsp;</td>"; echo "</tr>"; I catch (Zend_Service_Exception $e) ( die ($e->getMessage); Listing 5.8 Nutzung der Yahooi-Websuche 000 Mozilla Firefox 0 http://127.0.0.1 /-carsten/zf-buch/Yal ▼ u* |Gj’Google Galileo II - Memory Alpha p Galileo n. aus Memory Alpha, der freien deutschen Star-Trck-Datenbank ... Die Galileo n (NCC-1701/ der Stcmenflotrc der Föderation der... Zur Seite: http://memory-alpha.org/de/wiki/Galileo II gecachte Seite 23118 Modernes Webdesign I Web Standards in Germany Grafikerin und Webdesignerin Manuela Hoffmann fuhrt Sie mit diesem Wegweiser für ... wird voraussit 2008 bei Galileo erscheinen und zeigt, wie Sie ... Zur Seite: http7/www .webstandardsingermanv .de/2007/09/30/modcmes-webdesign/ gemachte Seite 23.118 Pisa Reiseführer - Wikitravel Der Open Source Reiseführer für Pisa mit aktuellen Informationen und Tipps über ... Flughafen (IATA- PSA), der nach Galileo Galilei benannt ist.... Zur Seite: htxp://wikitravcl .org/dc/Pisa gecachte Seite 23118 Webseiten erstellen für Einsteiger I Web Standards in Germany Dieses Buch vom Autor Daniel Mies zeigt in lockerer und verständlicher Sprache ... Bei Galileo bestelle Beiträge: Daniel Mies. Suchmaschinen-Optimierung ... Zur Seite: http J/www .webstandardsingermanv .de/2007/12/31 /webseitcn-erstellcn-fuer-cinsteigcr/ gecachte Seite 23118 Hildesheim Reiseführer - Wikitravel ........................................................................... • rJ- Fertig Stitf L N/A O YSIow 1.331s Abbildung 5.7 Treffer der Websuche zum Suchwort »Galileo« 239
e-bol.net 5 | Webservices 5.4.2 News-Suche mit Zend_Service_Yahoo Neben der Websuche unterstütz Zend_Servi ce_Yahoo auch die Suche im Nach- richtenangebot von Yahoo!. Die Nachrichten stammen aus den Angeboten ver- schiedener Zeitungen und Presseagenturen, welche von Yahoo! durchsucht wer- den. Auch diese Suchfunktion ist erfreulich einfach in der Handhabung. Sie müssen lediglich die Methode searchNews() aufrufen, welche den Suchbegriff als Parameter übergeben bekommt. Auch bei dieser Methode können Sie als zweiten Parameter ein Array mit Optionen übergeben, welches das Verhalten der Methode beeinflusst. Die Schlüssel, die Sie in diesem Array benutzen können, sind: type, resul ts, start, sort, 1anguage und site. Mit Ausnahme von sort sind diese alle in Tabelle 5.4 erläutert. In diesem Fall erhalten Sie allerdings maxi- mal 50 Ergebnisse pro Abfrage. Mit sort haben Sie die Möglichkeit, eine Sortier- reihenfolge zu bestimmen. Mit dem Default-Wert rank sortiert die Ausgabe nach dem Ranking des Treffers. Der Wert date hingegen führt dazu, dass der aktu- ellste Treffer als erster ausgegeben wird. Als Ergebnis der Abfrage erhalten Sie ein Objekt der Klasse Zend_Service_Yahoo_NewsResultSet, welches die Eigen- schaften totalResultsAvai1able, totalResultsReturned und firstResultPo- sition kennt. In diesen finden Sie die Gesamtzahl der Treffer, die Anzahl der zurückgegebenen Ergebnisse sowie den Offset des ersten Elements. Auch hier gilt, dass Sie das Objekt in einer foreach-Schleife nutzen können, wie am Anfang des Kapitels erwähnt. Pro Treffer erhalten Sie ein Objekt der Klasse Zend_Service_Yahoo_NewsResul t, dessen Eigenschaften Sie in Tabelle 5.6 fin- den. Schlüssel Erläuterung Title Titel des Treffers Summary Ausschnitt aus dem Inhalt Uri URL des Treffers CIickUrl URL, die für Links auf den Treffer genutzt werden soll NewsSource Name der Quelle des Treffers NewsSourceUrl URL des News-Angebots Language ISO-Sprach-Code des Treffers Publi shDate Veröffentlichungsdatum als UNIX-Timestamp Modi f1cati onDate Zeitpunkt der letzten Änderung als UNIX-Timestamp Thumbnai1 Hier ist bei einigen wenigen Anbietern ein Thumbnail auf ein Bild zu dem Artikel vorhanden. Die Informationen sind in Form eines Zend_ Servi ce_Yahoo_Image-Objekts enthalten. Informationen dazu finden Sie in Tabelle 5.7. Tabelle 5.6 Eigenschaften des NewsResult-Objekts 240
e-bol.net Yahoo!-Suche mit Zend_Service_Yahoo | 5-4 requi re_once 'Zend/Service/Yahoo.php'; header('Content-Type: text/html; charset=utf-8'); try { $yahoo = new Zend_Servi ce_Yahoo(" vAW6atX6Qhl)zB76D0TZqNSXg--"); // Bei 8 anfangen und 7 Treffer liefern $opts = array(’start'=>8. 'results'=>7); // Suche nach den Bush $results = $yahoo->newsSearch(utf8_encode('Bush'),$opts); if (0 < $results->totalResultsReturned) { echo "<table width='150'>"; foreach ($results as $result) echo "<tr>"; echo "<td colspan=’2'><b>$result->Title</b></td>"; echo "</tr>"; echo "<tr>"; // Haben wir ein Bild? if ($result->Thumbnai1) { // Dann koennen wir es auch ausgeben echo "<td>"; echo "<img src='ISresult->Thumbnai1->Url}'"; echo " width='|Sresult->Thumbnai1->Width}' "; echo " height='{$result->Thumbnai1->Height}’ >"; el se // Kein Bild also ein leeres Tabellenfeld echo "<td >&nbsp;</td>"; } echo "<td>$result->Summary"; echo "<p><a href=’$result->ClickUrl'>$result->Url</a></p>"; echo "</tr>"; echo "<tr>"; echo "<td colspan=’2'>&nbsp;</td>"; echo "</tr>"; } catch (Zend_Service_Exception $e) 241
e-bol.net 5 | Webservices ( die ($e->getMessage); I Listing 5.9 News-Suche bei Yahoo! Die Ausgabe des Scripts sehen Sie in Abbildung 5.8. Mozilla Firefox V’ • e 0 http://127.0.0.1/~carsten/zf-buch/Yahoo/zwei.php ▼ Google Fatber of Bush tax cuts: Recession likely Martin Feldstein, the Harvard cconomist credited with bcing one of the fathers of the Bush administration tax cuts, says the U .S. economy is now likely to slip into a recession, and (hat avoiding one will take a new round of tax cuts and interest rate cuts from the Federal Reserve. hffl?y/moncy ,ctm ,rotu/rs«?litk/20Q81 fl7/pc w^^nomy/fcldsKin/index Jitm?^üon~mQnty law?< Bush urges 'good faith' Kenya dialogue US President George W. Bush on Monday urged Kenya s govemment and Opposition to hold "good faith talks and urged an end to violence while leaders seek "a lasting political solution." http://ncws.yahoo.eom/s/afp/20080107/pl afp/kenyavoteunrestuswhousc 080107235607 Olmert, Abbas try to close gaps abead of Bush v isit Israeli Prime Minister Ehud Olmert and Palestinian President Mahmoud Abbas meet on Tucsday in a last-minute attempt to get stalled pcacc talks going before a visit by U.S. President George W. Bush. http://news.vahoo.eom/s/nm/20080107/ts nm/palestinians Israel dc 8 Bush: Keep Laxes kw in uncertain economic times Citing "incrcasingly mixed" signs on the U.S. economy's hcalth, President George W. Bush on Monday urged Congress to make permanent tax cuts enacted since he took Office, saying low taxes were the best tonic. http7/ncws.vahoo.com/s/nm/20080107/pl nm/bush economy growth dc 4 Fertig O fe YSIow 1.241s Abbildung 5.8 Ergebnis der News-Suche 5.4.3 Bildersuche mit Zend_Service_Yahoo Wie verschiedene andere Suchmaschinen bietet auch Yahoo! eine Bildersuche an. Natürlich können Sie nicht in den Bildern selbst nach Personen oder Formen suchen. Nur die Texte, die um Bilder herum platziert sind, können durchsucht werden. Sie lassen einen Rückschluss auf das zu, was auf dem Bild zu sehen ist. Dabei kann es natürlich auch immer wieder einmal zu Fehlinterpretationen kom- men. Die Methode, um Bilder zu suchen, heißt imageSearch() und bekommt die Such- begriffe direkt als ersten Parameter übergeben. Auch in diesem Fall können Sie als zweiten Parameter ein Array mit Parametern übergeben. Die gültigen Schlüs- sel in diesem Zusammenhang sind results, start, format, adult_ok, colora- tion und site. Die Schlüssel resul ts, start, adult_ok und site sind identisch 242
e-bol.net Yahoo!-Suche mit Zend_Service_Yahoo | 5-4 mit denen der Websuche, die Sie in Tabelle 5.4 finden. Wobei allerdings auch hier die Einschränkung gilt, dass maximal 50 Ergebnisse pro Suchanfrage gelie- fert werden. Mithilfe von format haben Sie die Möglichkeit zu definieren, welche Art von Grafikdatei Sie suchen. Der Default-Wert any liefert Ihnen alle Formate. Andere mögliche Werte sind bmp, gi f, jpeg und png. Mit dem Schlüssel col orati on kann definiert werden, ob die Treffer bunt (Wert: col or) oder schwarz-weiß (Wert: bw) sein sollen. Das Rückgabeobjekt gehört zu Klasse Zend_Servi ce_Yahoo_ImageResul tSet und liefert in den Eigenschaften totalResultsAvailable, totalResultsReturned und firstResultPosition die Gesamtzahl der Treffer, die Anzahl der zurückge- gebenen Treffer sowie den Offset des ersten zurückgegebenen Ergebnisses. Übergeben Sie das Objekt an eine Schleife oder greifen Sie über die Methoden der SPL auf die einzelnen Bilder zu, erhalten Sie pro Bild jeweils ein Objekt der Klasse Zend_Service_Yahoo_Image, deren Eigenschaften Sie in Tabelle 5.7 fin- den. Eigenschaft Bedeutung Summary Kurzbeschreibung des Bildes RefererUrl URL der Seite, auf der das Bild eingebettet ist Fi 1eSi ze Größe der Bilddatei in Byte Fi 1eFormat Dateiformat des Bildes Height Höhe des Bildes in Pixeln Width Breite in Pixeln Thumbnai1 Verweis auf ein Thumbnail aus dem Yahool-Cache. Die Eigenschaft Uri enthält die URL des Bildes, und die Eigenschaften Hei ght und Width die Höhe und Breite. Title Titel des Bildes, üblicherweise der Dateiname Uri URL des Bildes CIi ckUrl URL, die zum Anklicken verwendet werden soll. Zurzeit identisch mit dem Feld Uri. Tabelle 5.7 Eingenschaften eines Image-Objekts Bei der Nutzung der Bildersuche beachten Sie bitte, dass die Informationen nicht immer ganz so aktuell wie die der Web- oder Nachrichtensuche sind. Insbeson- dere Thumbnails können schon einmal veraltet sein. Es kann auch passieren, dass noch ein Treffer inklusive Thumbnail zurückgegeben wird, obwohl das eigentli- che Bild schon nicht mehr da ist. 243
e-bol.net 5 | Webservices requi re_once 'Zend/Service/Yahoo.php'; header('Content-Type: text/html; charset=utf-8'); try { $yahoo = new Zend_Service_Yahoo("vrJ4xH86JNA6Q0TZqNSg--"); $results = $yahoo->imageSearch(utf8_encode('Patrick Star’)): if (0 < $results->totalResultsReturned) { echo "<table width='150; foreach (Sresults as $result) echo "<tr>": echo "<td>": echo "<img src='{Sresult->Thumbnai1->Url}’"; echo " width=’|$result->Thumbnail->Width}’ "; echo " height=’($result->Thumbnai1->Height)' >": echo "</td>"; echo "<td> $result->Title<br>"; echo nl2br( $resul t->Suminary). "<br>"; echo "Dateigröße: $result->Fi1eSize Bytes<br>"; echo "Seite:<a href='$result->RefererUrl'> $result->RefererUrl</a><br>"; echo "Bild:<a href='$result->ClickUrl’> Sresult->Ur1</a>": echo "</td></tr>"; echo "<tr>": echo "<td colspan=’2'>&nbsp:</td>": echo "</tr>"; } } catch (Zend_Service_Exception $e) { die ($e->getMessage): } Listing 5.10 Bildersuche bei Yahoo! 244
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5.5 Abbildung 5.9 Ergebnis der Bildersuche 5.5 Zugriff auf Google-Dienste mit Zend_Gdata Der Suchmaschinen-Gigant Google bietet neben der eigentlichen Suchmaschine noch viele andere Dienste im Internet an. So bietet Google Dienste wie eine Tabellenkalkulation, eine Textverarbeitung oder auch einen Terminplaner an. Des Weiteren gehören inzwischen auch andere Dienste zum Google-Konzern, bei denen das vielleicht nicht auf Anhieb ersichtlich ist, wie beispielsweise YouTube. Viele dieser Dienste können über eine einheitliche Schnittstelle, der Google Data API, angesprochen werden. Diese API, die auch kurz GData genannt wird, basiert auf dem Atom Publishing Protocol. GData unterstützt zurzeit noch nicht alle Google-Angebote, sodass es beispielsweise leider noch nicht möglich ist, darüber die eigentliche Suchmaschine zu nutzen. Momentan unterstützt das Zend Framework den Zugriff auf die APIs von: Google Calendar, Google Spreadsheets, Picasa, Youtube, Google Base und Google Docu- ments. Die Zugriffsmöglichkeiten sind unterschiedlich umfangreich implemen- tiert. Ich habe mich hier dafür entschieden, mich auf die Vorstellung der Calen- dar- und Spreadsheets-Pakete zu beschränken. Diese kennen schon recht viele 245
e-bol.net 5 | Webservices Möglichkeiten. Haben Sie die Nutzung dieser Pakete verstanden, so können Sie sich sicherlich schnell in die Nutzung der anderen Pakete einarbeiten. Resultierend aus der Tatsache, dass die Dienste alle über dieselbe API angespro- chen werden, gibt es natürlich viele Gemeinsamkeiten bei den entsprechenden Paketen des Zend Frameworks. Daher möchte ich Ihnen zunächst die Methoden und Techniken vorstellen, die bei allen Google-Klassen Verwendung finden, und dann auf die Authentifikation eingehen. 5.5.1 Allgemeines zu Zend_Gdata Google Data basiert auf dem Atom Publishing Protocol. Dabei handelt es sich um ein XML-basiertes Protokoll, bei dem die Daten über das HTTP-Protokoll ausge- tauscht werden. Der Zugriff auf die verschiedenen Dienste wird über Feeds realisiert. Das heißt, wenn Sie auf eine bestimmte Informationsressource zugreifen wollen, ist dafür immer eine Feed-URL definiert, wie beispielsweise http://www.google.com/ calendar/feeds/default/owncalendars/full. In einer solchen URL sind verschie- dene Informationen enthalten. In diesem Fall wird der Zugriff auf die Kalender eines Benutzers angestrebt. Abhängig davon, welche Informationen Sie benötigen, müssen Sie sich gegenü- ber dem System authentifizieren. Wenn Sie beispielsweise öffentliche Daten aus einem Blog auslesen, ist keine Anmeldung am System notwendig. Möchten Sie aber einen neuen Eintrag in Ihrem Kalender anlegen, so müssen Sie sich natürlich zuvor anmelden. Übrigens wird auch das Schreiben von Daten über solche URLs verwaltet. Die Nutzung der verschiedenen Google-Dienste ist in Zend_Gdata - da die glei- chen Basisklassen genutzt werden - recht ähnlich implementiert. Vor diesem Hintergrund werde ich diejenigen Funktionalitäten, die in allen Paketen vorkom- men, lediglich bei Google Calendar erläutern. Auch wenn Sie sich vielleicht nur mir Spreadsheets beschäftigen wollen, sollten Sie vorher das Kapitel zu Google Calendar lesen. 5.5.2 Authentifikation Die Authentifikation gegenüber der Google-API kann auf zwei Wegen erfolgen. Zum ersten können Sie die »übliche« Vorgehensweise mithilfe von Benutzerna- men bzw. E-Mail-Adresse und Passwort nutzen. Die zweite Möglichkeit ist die Anmeldung mithilfe von Googles Account Authentification. 246
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Für beide Vorgehensweisen ist jeweils eine eigene Klasse definiert, in der es eine Methode namens getHttpCl i ent() gibt, mit welcher ein neuer, authentifizierter Client abgeleitet werden kann. Authentifikation via Client-Log-in Eine Authentifikation mithilfe eines Client-Log-ins bietet sich immer dann an, wenn unterschiedliche Benutzer eine Applikation nutzen sollen, die sich mithilfe ihrer E-Mail-Adresse und ihres Passwortes anmelden können. In den folgenden Beispielen wurden E-Mail-Adresse und Passwort direkt in den Code integriert, um die Strukturen einfach zu halten. Ein Client-Log-in, mit dem Sie sich bei Google Calendar anmelden können, sieht im einfachsten Fall so aus: Semail = 'zf-buch@netviser.de': Spasswd = 'total geheim*; Sclient = Zend_Gdata_ClientLogin::getHttpClient( $emai1,$passwd,’ei'); Seal = new Zend_Gdata_Calendar(Seiient): In diesem kleinen Beispiel wird ein neuer Client generiert, indem der statischen Methode getHttpCl i ent() die E-Mail-Adresse, also sozusagen der Log-in-Name, und das Passwort übergeben werden. Der dritte Parameter, der String 'ei’, legt fest, dass eine Verbindung mit dem Calendar-Dienst aufgebaut werden soll. Hier können Sie eine der Konstanten aus Tabelle 5.8 nutzen, wobei Sie allerdings auch auf den universellen String xapi zurückgreifen können. Damit können Sie sich zu allen Services verbinden. Abkürzung Service cl Calendar blogger Blogger gbase Google Base wi se Google Spreadsheet apps Google Apps 1 h2 Picasa Web Albums youtube YouTube xapi Standard Client Tabelle 5.8 Abkürzungen für Google-Dienste Allerdings ist diese Vorgehensweise nicht perfekt. Neben den »üblichen« Excep- tions, die auftreten können, besteht bei dieser Form der Authentifikation noch 247
e-bol.net 5 | Webservices das Problem, dass eine Exception der Klasse Zend_Gdata_App_CaptchaRequi red- Exception geworfen werden kann. Das ist dann der Fall, wenn Google die Nut- zung eines CAPTCHA verlangt. Die Nutzung eines CAPTCHA verlangt Google immer dann, wenn zu viele Log-in-Versuche in zu kurzer Zeit stattgefunden haben. Sollte dieser Fall eintreten, so können Sie mit der Methode get- CaptchaUrK) die URL des CAPTCHA-Bildes auslesen, und die Methode get- CaptchaToken() liefert ein Token, welches Sie bei der nächsten Anfrage zusam- men mit der Benutzereingabe an den Server senden müssen, damit diese verifiziert werden kann. requi re_once 'Zend/Gdata/ClientLogin.php'; requi re_once 'Zend/Gdata/Calendar.php *; Semail = 'zf-buch@netviser.de'; Spasswd = 'total geheim*; Sservice = 'ei'; $cli ent = null; Ssource = Zend_Gdata_ClientLogin::DEFAULT_SOURCE; // Wurden Daten aus dem Formular übergeben? if (true — isset ($_POST['captcha'])) { // Daten aus Formular übernehmen $1ogin_captcha = $_POST['captcha; $login_token = $_POST[’token']; } el se { // Keine Daten erhalten => null übergeben $1ogi n_captcha = null; $login_token = null; } try { $client = Zend_Gdata_ClientLogin:rgetHttpClient($emai1, $passwd, $service, $client, $source, $1ogin_token, $1ogin_captcha); } catch (Zend_Gdata_App_CaptchaRequiredException $e) { // Google verlangt ein CAPTCHA => Formular ausgeben echo 'Bitte geben Sie den Code ein, den Sie auf dem Bi 1d sehen.<br>'; 248
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 echo "<form method=’post' action=’$_SERVER[PHP_SELF]'>"; // CAPTCHA-Bild ausgeben echo '<img src="'.$e->getCaptchaUrl().’" ><br>'; echo 'Code: <input type="text" name="captcha"Xbr>': // Token in einem versteckten Feld übergeben echo ’Cinput type="hidden" value="’.$e->getCaptchaToken(). '" name="token" ><br>’; echo '<input type="submit" value="Abschicken">'; echo '</form>'; } catch (Zend_Gdata_App_AuthException $e) { // Hier werden Authentifikationsfehler abgefangen die ('Bei der Authentifikation trat folgender Fehler auf: $e->getMessage()); } catch (Exception $e) { // Hier werden sonstige Exceptions abgefangen die ('Der folgende Fehler ist aufgetreten: '.$e->getMessage()): } Seal = new Zend_Gdata_Calendar(Seiient): Listing 5.11 Authentifikation via Client-Log-in In Listing 5.11 finden Sie eine etwas komplexere Lösung zum Ableiten eines Cli- ents. Die Methode getHttpCl ient() wird auch hier wieder genutzt, wobei in diesem Beispiel alle Parameter genutzt werden und nicht nur die obligatorischen. Mit dem vierten Parameter könnten Sie ein Objekt der Klasse Zend_Http_Cl i ent übergeben. Das bietet sich dann an, wenn Sie bereits ein Objekt dieser Klasse besitzen. Übergeben Sie an dieser Stelle null oder lassen Sie den Parameter ganz wegfallen, leitet die Methode selbst ein Objekt der Klasse ab. Bei dem fünften Parameter, in diesem Beispiel also die Variable $ source, handelt es sich um die »Kennung« der Applikation. In diesem Beispiel wird der Wert der Konstante DEFAULT_SOURCE übergeben, welche standardmäßig mit dem String Zend - ZendFramework belegt ist. An dieser Stelle können und sollten Sie allerdings die Kennung Ihrer Applikation übergeben. Entsprechend der Google-Dokumen- tation sollte die Kennung nach folgendem Schema aufgebaut sein: ’firma- applikation-versionsID'. An diesen Parameter schließen sich die Parameter an, mit denen das Token und der Code vom CAPTCHA-Bild übergeben werden. Diese sind natürlich nur dann 249
e-bol.net 5 | Webservices erforderlich, wenn Google nach einem CAPTCHA verlangt. Mit anderen Worten: Beim vorhergehenden Aufruf der Methode wurde eine Ausnahme des Typs Zend_Gdata_App_CaptchaRequi redException geworfen. In dem Fall wird dann dasjenige Formular ausgegeben, welches in dem catch-Block definiert wird. Die URL des Bildes und das Token werden mithilfe der Methoden getCaptchaUrl () und getCaptchaToken() ausgelesen und in das Formular integriert. Wie ein sol- ches CAPTCHA-Formular aussehen kann, sehen Sie in Abbildung 5.10. R n o http://127.0.0.1/~c...f-buch/CData/l.php fei oBfov Google » CD Planet PHP (137) Apple (23)v Amazon eBay » Bitte geben Sie den Code ein, den Sic auf dem Bild sehen, f Abschicken Abbildung 5.10 CAPTCHA bei einem Client-Log-in Wird das Formular abgeschickt, werden die Daten aus dem Formular für den nächsten Verbindungsaufbau genutzt. Der zweite catch-Block dient dazu, Authentifikationsfehler, also wenn die Kom- bination aus Log-in und Passwort nicht gültig ist, abzufangen. Leider liefert die Google-Dokumentation momentan noch keine exakten Informationen, welche Codes hier zurückgeliefert werden. Sie enthält lediglich die Information, dass die Fehlermeldung eine Erläuterung des Fehlers enthält. Das Beispiel aus Listing 5.11 ist natürlich ein klein wenig realitätsfremd, da die E-Mail-Adresse und das Passwort hart in die Anwendung codiert sind. Bei einer Anwendung im »echten Leben« würden diese Daten sicher eher aus einem For- mular oder einer Konfigurationsdatei übernommen. Authentifikation via Account-Authentication Die Account-Authentication nutzt eine gänzlich andere Vorgehensweise, die auf den ersten Blick ein wenig befremdlich erscheinen mag. Die Idee ist allerdings gut und bringt einige Vorteile mit sich. Sie wird übrigens auch als AuthSub bezeichnet, da Google eine solche Authentifikation dann durchführt, wenn ein AuthSub-Request erfolgte. 250
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Ihre Applikation leitet den Benutzer zunächst auf eine Seite von Google, wobei die Applikation ihre eigene URL mit übergibt. Auf der Google-Seite wird der Benutzer dann gefragt, ob er der Applikation Zugriff auf die Daten gewähren möchte, und loggt sich ein. Sind die Daten korrekt, schickt Google den Benutzer zurück zur ursprünglichen Anwendung, wobei dieser ein Token übergeben wird. Bei diesem Token handelt es sich um ein »One-Time-Use-Token«, das Sie nur für einen Request nutzen können. Da Sie in den meisten Fällen aber mehrere Anfra- gen benötigen werden, sollten Sie die eine Anfrage, die Ihnen zur Verfügung steht, dazu nutzen, um aus dem One-Time-Use-Token ein Session-Token zu machen, das dann für die gesamte Sitzung gültig ist. Mithilfe dieses Session- Tokens kann die Anwendung sich bei allen folgenden Requests authentifizieren. Diese Vorgehensweise bietet sich immer dann an, wenn viele verschiedene Benutzer eine Anwendung nutzen sollen. Insbesondere dadurch, dass das Log-in auf einer Seite von Google erfolgt, haben die Benutzer auch ein größeres Gefühl der Sicherheit. So viel zur Theorie. Die URL, die Ihre Applikation aufrufen muss, damit sich der Benutzer authentifizieren kann, setzt sich aus verschiedenen Informationen zusammen, die Sie an die statische Methode getAuthSubTokenUri (), die in der Klasse Zend_Gdata_AuthSub definiert ist, übergeben sollten. Als ersten Parameter übergeben Sie die URL Ihrer Applikation, damit Google den Benutzer auch wie- der zurückschicken kann. Der zweite Parameter ist die URL der Google-Anwen- dung, die genutzt werden soll, da Google natürlich wissen muss, für welchen Dienst authentifiziert werden soll. Danach folgen zwei optionale Parameter, die entweder 0 oder 1 als Wert haben. Der erste von beiden legt fest, ob ein sicheres Token genutzt werden soll. Diese Möglichkeit steht allerdings nur dann zur Verfügung, wenn Sie Ihre Applikation vorher bei Google registriert haben.8 Somit wird an dieser Stelle üblicherweise eine 0 zu finden sein. Der zweite Wert teilt Google mit, ob das One-Time-Use- Token in ein Session-Token konvertiert werden darf. Hier wird also üblicher- weise eine 1 stehen. Der Methodenaufruf liefert Ihnen die URL zurück, auf die der Benutzer weiterge- leitet werden muss. Das kann über einen Link oder direkt durch die Funktion header() geschehen. Nachdem der Benutzer sich angemeldet und zugestimmt hat, die Applikation zu authentifizieren, wird er zurückgeleitet, wobei das One-Time-Use-Token über die 8 Möchten Sie Ihre Applikation registrieren lassen, finden Sie hier weitere Informationen: http ://code .google .com/apis/accounts/RegistrationFor Web Apps .html 251
e-bol.net 5 | Webservices URL übergeben wird. Das wiederum übergeben Sie an die statische Methode getAuthSubSessionToken(), die es in ein Session-Token konvertiert. Ein solcher Log-in-Mechanismus kann dann so aussehen: < ? php requi re_once('Zend/Gdata/AuthSub.php'); requi re_once(’Zend/Gdata/Calendar.php'); sessi on_start(); // Ist die Applikation noch nicht authentifiziert? if (false —== isset($_SESSION['token'])) { // Wurde ein Token über die URL übergeben? if (false === isset($_GET[’token'])) { // Kein Token über URL => dann Link ausgeben // URL der Kalender-Applikation $url_kalendar =’http://www.google.com/calendar/’. 'feeds/default/pri vate/ful1' ; // URL der eigenen Applikation $url_applikation = 'http://'. $_SERVER[’SERVER_NAME']. $_SERVER[’REQUESTJJRI’]: $sicheres_token = 0: $session_token_ok = 1: // Link ausgeben den der User anklicken kann $google_url = Zend_Gdata_AuthSub:rgetAuthSubTokenUri ( $url_applikation, $url_kalendar, $sicheres_token, $session_token_ok); echo "Klicken Sie <a href='SgoogleUri’>hier</a> um die Applikation zu authentifizieren."; exit(); el se { // Konvertieren des One-Time-Use-Tokens in ein Session-Token // und Speichern in der Session try $_SESSION[’token'] = Zend_Gdata_AuthSub:: getAuthSubSessi onToken($_GET['token']); 1 catch (Zend_Gdata_App_AuthException $e) 252
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata 5-5 die ('Konnte One-Time-Use-Token nicht in Session-Token konvertieren <br>Grund: '.$e->getMessage()): I // Client mit Token initialisieren $client = Zend_Gdata_AuthSub::getHttpCli ent($_SESSION C’token']): // Calendar-Client ableiten $cal = new Zend_Gdata_Calendar($client); Listing 5.12 Authentifikation mit AuthSub In Abbildung 5.11 und Abbildung 5.12 sehen Sie die Google-Seiten, die der Benutzer in diesem Beispiel zu sehen bekommt. A O O https://www.google.com/accounts/AuthSub...efault/private/full&secure=0&session = l A [ 4 A|M^jfcgjM^JK*|https://www.google.com/accounts/AuthSijbReqQj^Ct* alogger Q] □□ Planet PHP (118) Apple (21)▼ Amazon eBay Yahoo’ News Sie müssen sich anmelden, um den Dienst eines Drittanbieters für den Zugriff auf Ihr Konto zu autorisieren. Melden Sie sich hier an: Go gle Konto E-Mail: jf-buch@netviser.de Passwort: [.....| □ Auf diesem Computer merken. Anmelden ) Ich kann nicht auf .mein Konto zugr eifer Abbildung 5.11 Log-in bei Google Um den Benutzer bzw. die Applikation wieder auszuloggen, müssen Sie Google mitteilen, dass das Token ungültig gemacht werden soll. Dazu ist die statische Methode AuthSubRevokeToken() deklariert, der Sie einfach das Token überge- ben können: Zend_Gdata_AuthSub::AuthSubRevokeToken($_S ESS ION['token’]); unset($_SESSION['token']); 253
e-bol.net 5 | Webservices Abbildung 5.12 Information über die Weiterleitung Aus Gründen der Übersichtlichkeit werde ich in den folgenden Kapiteln immer eine Authentifikation via Client-Log-in verwenden, wobei ich auf die Prüfung einer CAPTCHA-Exception verzichte. Exception Handling bei Zend_Gdata Die Gdata-Pakete verhalten sich beim Exception Handling ein wenig anders als die anderen Klassen im Zend Framework. Aufgrund der Vielzahl der Klassen wären so viele unterschiedliche Exceptions auch schlecht handhabbar. Momentan nutzen die Gdata-Pakete die folgenden Exceptions: ► Zend_Gdata_App_AuthException Diese Exception-Klasse wird immer dann genutzt, wenn das Log-in nicht erfolgreich war, weil es nicht möglich war, sich mit der Kombination aus Benutzernamen und Passwort anzumelden. ► Zend_Gdata_App_CaptchaRequi redException Nutzen Sie ein Client-Log-in, kann es passieren, dass Google bei zu vielen Log- in-Versuchen in zu kurzer Zeit die Nutzung eines CAPTCHA verlangt. In die- sem Fall wirft Zend_Gdata die obige Exception. 254
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 ► Zend_Gdata_App_HttpException Eine Exception dieses Typs wird geworfen, wenn ein Fehler bei der Kommu- nikation mit Google auftritt. Das kann beispielsweise dann passieren, wenn Sie eine falsche URL nutzen. ► Zend_Gdata_App_Inval1dArgumentExcepti on Dieser Typ von Exception begegnet Ihnen dann, wenn Sie bei einer Abfrage falsche Parameter spezifizieren. ► Zend_Gdata_App_BadMethodCal1Exception Diese Exception wird genutzt, wenn eine HTTP-Methode genutzt wurde, die der aktuell genutzte Dienst nicht unterstützt. Dieser Typ von Exception sollte nicht auftreten, solange Sie die vordefinierten Methoden nutzen. 5.5.3 Nutzung von Google Calendar Mit Google Calendar bietet Google einen sehr umfangreichen Terminmanager an, der über das Internet genutzt werden kann. Dank modernster Technik ist er ähnlich komfortabel wie die Terminverwaltung von Outlook und ähnlichen Tools. Sollten Sie sich noch nicht mit Google Calendar beschäftigt haben, wäre das jetzt ein guter Zeitpunkt. Es dürfte es Ihnen leichter machen, die nachfolgen- den Funktionen zu verstehen. Einen neuen Account können Sie unter http://www.google.com/calendar anlegen. Aufgrund der Komplexität der API kann ich sie in diesem Zusammenhang nicht komplett erläutern. Das Kapitel wird Ihnen aber einen guten Einstieg bieten. Sollten Sie weitere Informationen benötigen, dann ist die Dokumentation der Google-API eine gute Anlaufstelle. Sie finden sie unter der Adresse http://code. google. com/apis/calendar/developers_guide_protocol. html. Auslesen von Events Das Auslesen von Terminen, die im Kalender eingetragen sind, ist recht einfach. Nach der Authentifikation müssen Sie der Klasse nur mitteilen, dass Sie die Ter- mine auslesen wollen. Dabei erhalten Sie ein Feed-Objekt zurück. In diesem Fall handelt es sich um ein Objekt der Klasse Zend_Gdata_Cal endar_EventFeed. Hier- bei handelt es sich um eine Kind-Klasse der Klasse Zend_Gdata_Feed. Alle Funk- tionalitäten, die für Sie wichtig sind, sind auch in dieser Eltern-Klasse realisiert. Auch die Klasse der anderen Feeds, die Sie an anderer Stelle zurückerhalten, sind von dieser Klasse abgeleitet, sodass Sie eine weitestgehend einheitliche API haben. 255
e-bol.net 5 | Webservices Die Klasse Zend_Gdata_Feed implementiert das SPL-Interface Iterator, sodass Sie das entsprechende Objekt direkt in einer foreach-Schleife nutzen können. Innerhalb der Schleife stehen dann die einzelnen Objekte für die einzelnen Ein- träge zur Verfügung. In diesem Fall handelt es sich dabei um die Klasse Zend_ Gdata_Cal endar_EventEntry. Auch hier gilt, dass es eine Elternklasse gibt, die einen Großteil der Methoden deklariert. In diesem Fall heißt sie Zend_Gdata_ Entry. In der Schleife laufen Sie nun über die einzelnen Einträge aus dem Kalender. Wie bei einem Feed üblich, handelt es sich dabei um eigenständige XML-Knoten, die weitere Elemente beinhalten. Um die dort enthaltenen Informationen auszule- sen, gibt es mehrere Möglichkeiten. Möglichkeit eins ist, dass Sie das Entry- Objekt nutzen und das fragliche XML-Element einfach als Eigenschaft anhängen. Das könnte beispielsweise so aussehen: foreach ($eintraege as Seintrag) { echo Seintrag->title."<br>"; 1 Diese Schleife würde also von jedem Entry-Objekt die Eigenschaft title bzw. den Inhalt des XML-Elements title ausgeben. Die andere Möglichkeit ist, dass Sie die Methode getTi tl e() aufrufen, die Ihnen auch den Inhalt zurückgibt. Die Methode getTi tl e() gehört zu den magischen Methoden, die das System kennt. Es gibt eine ganze Menge dieser magischen Methoden, die oft auch genutzt wer- den, um neue Objekte abzuleiten. Diese Factoiys beginnen dann jeweils mit new, wie Sie noch sehen werden. Nun stellt sich die interessante Frage, woher man überhaupt weiß, dass es das Element title gibt. Die Frage ist leider nur mithilfe der Google-Dokumentation zu beantworten. Dort finden Sie für die verschiedenen Dienste Informationen, wie die XML-Nachrichten aufgebaut sind. Sie finden Beispiele wie dieses: <entry> <id>http://www.google.com/calendar/feeds/jo@gmai1.com/pri vate /ful1</i d> <publi shed>2006-03-30T22:00:00.000Z</publi shed> <updated>2006-03-28T05:47:31.0 0 0Z</updated> <title type=’text'>Lunch with Darcy</title> Ccontent type='text'>Lunch to discuss future plans.</content> <link rel='self' type=*application/atom+xml’ href=’http://www.google.com/calendar/feeds/jo@gmai1.com/pri vate- magi cCooki e/full/entryID'X/link> <author> 256
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 <name>Jo March</name> <emai1>jo@gmai1.com</emai 1 > </author> <gd:transparency val ue='http://Schemas.google.eom/g/2005#event.opaque'> </gd:transparency> <gd:eventStatus val ue='http://Schemas.google.com/g/2005#event.confi rmed’> </gd:eventStatus> <gd:when startTime=’2006-03-30T22:00: 00.000Z' endT ime='2006-03-30T23:00:00.000Z’></gd:when> <gd:where></gd:where> </entry> Dieses Beispiel aus der Google-Dokumentation habe ich deutlich gekürzt, aber die wichtigsten Elemente können Sie erkennen. Ein wenig problematisch ist die Tatsache, dass einige Elemente in Form von Arrays bereitgestellt werden und andere nicht. Leider konnte ich weder in der Dokumentation von Google noch in der des Zend Frameworks einen Hinweis finden, wann ein Element ein Array ist und wann nicht. Hier kann ich Sie leider nur darauf verweisen, dass Sie dies ausprobieren. So werden die Elemente <author> oder <gd :when > beispielsweise als Array mit einem Element vorgehal- ten. Um nun einen Kalender komplett auszulesen, könnten Sie folgendermaßen vorgehen: requi re_once 'Zend/Gdata/ClientLogin.php'; requi re_once ’Zend/Gdata/Calendar.php'; require_once 'Zend/Gdata/Calendar/EventQuery.php'; require_once ’Zend/Date.php’; header ('Content-Type: text/html; charset=utf-8’); Semail = 'zf-buch@netviser.de': Spasswd = 'total geheim'; Sservice = 'ei': $cli ent = null; Ssource = Zend_Gdata_ClientLogin::DEFAULT_SOURCE; try { tclient = Zend_Gdata_ClientLogin::getHttpCl ient( $emai 1, $passwd, tservice, $client, $source): } catch (Exception $e) 257
e-bol.net 5 | Webservices die ('Folgender Fehler trat auf: ' . Se->getMessage()): Seal = new Zend_Gdata_Calendar(Seiient): Seintraege = Seal->getCalendarEventFeed(): echo "<table>"; foreach (Seintraege as Seintrag) { echo "<tr><td>Titel: </td>"; echo "<td>".Seintrag->title."</td></tr>"; echo "<tr><td>Ort: </td>"; echo "<td>". Sei ntrag->where[O]. "</tdX/tr>"; echo "<trXtd>Kommentar: </td>": echo "<td>".Seintrag->content."</tdX/tr>"; echo "<trXtd>Eingetragen von: </td>": echo "<td>".Seintrag->author[0]->name."</tdX/tr>"; echo "<trXtd>Startzeit: </td>": Sdat_start = (string) new Zend_Date(Seintrag->when[O]->startTime, Zend_Date::ISO_8601, 'de_DE*); echo "<td>". $dat_start. "</tdX/tr>”; echo "<trXtd>Endzeit: </td>"; Sdat_ende = (string) new Zend_Date($eintrag->when[O]->endTime, Zend_Date::ISO_8601, 'de_DE*); echo "<td>". $dat_ende. "</tdX/tr>"; echo "<trXtd>Eingetragen: </td>"; Sdat_eingetragen = (string) new Zend_Date($eintrag->published, Zend_Date::ISO_8601, 'de_DE*): echo "<td>". $dat_ei ngetragen. "</tdX/tr>"; echo "<trXtd>Aktualisiert: </td>": Sdat_aktualisiert = (string) new Zend_Date(Seintrag->updated, Zend_Date::ISO_8601, 'de_DE*): echo "<td>". $dat_aktual i siert. "</tdX/tr>"; echo XtrXtd col spn=’2' >&nbsp;</tdX/tr>"; } echo "</table>"; Listing 5.13 Auslesen von Kalenderdaten 258
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Da die Uhrzeiten in einem ISO 8601-kompatiblen Format vorliegen, habe ich hier der Einfachheit halber zur Klasse Zend_Date gegriffen, um sie zu konvertie- ren. Die Ausgabe der Kalenderdaten sehen Sie in Abbildung 5.13. ry O http://127.0.0.1...uch/CData/l_l.php CD w Q Titel: Flug nach Berlin Ort: Hannover Flughafen Kommentar Flugnummer LH-42 Eingetragen von: Carsten Möhrkc Startzeit 15.012008 20:00:00 Endzeit 15.01.2008 22:00:00 Eingetragen: 05.01.2008 19:27:05 Aktualisiert: 05.01.2008 21:25:16 Titel: Lottoschein abgeben Ort: Lottoladcn Kommentar Eingetragen von: Carsten Möhrkc Startzeit 13.01.2008 00:00:00 Endzeit: 14.01.2008 00:00:00 Eingetragen: 05.01.2008 19:25:25 Aktualisiert: 05.012008 2124:54 Titel: Zahnarzt Ort Bielefeld Kommentar Vorher die Valium nehmen! Eingetragen von: Carsten Möhrkc Startzeit 14.01.2008 15:00:00 Endzeit: 14.012008 17:00:00 Eingetragen: 05.01.2008 20:27:30 Aktualisiert: 05.01.2008 20:27:30 — ▼ Abbildung 5.13 Ausgabe der Kalenderdaten Wie Sie sehen, ist der Zugriff auf die Daten nicht sonderlich kompliziert. Die ein- zelnen Elemente der Einträge liegen in Form von Eigenschaften bzw. Objekten vor. Einige der Eigenschaften sind allerdings etwas komplexer als andere. So ent- hält das Element when beispielsweise zwei Informationen in Form von Attribu- ten: Die Start- und die Endzeit des Termins. Diese komplexeren Elemente sind innerhalb von Zend_Gdata als eigene Klassen umgesetzt. Klassen, die für alle Dienste relevant sind, finden Sie im Unterverzeichnis Zend/Gdata/Extension. Klassen, die nur für bestimmte Dienste genutzt werden, finden Sie im jeweiligen Unterverzeichnis des Dienstes. Das heißt, die Klassen für Calendar finden Sie 259
e-bol.net 5 | Webservices unter Zend/Gdata/Calendar/Extension. Die Tatsache, dass es sich dabei um eigene Klassen handelt, ist eigentlich nicht weiter wichtig. Ich wollte es nur erwähnt haben, damit Sie in den Klassen ein wenig stöbern, um noch weitere Funktionalitäten zu entdecken. Abbildung 5.14 Der Kalender bei Google Ein wichtiger Punkt sind gelöschte Termine. Diese finden im letzten Beispiel noch keine Beachtung. Termine, die Sie gelöscht haben, werden nicht sofort gelöscht. Sie werden zunächst nur als gelöscht markiert. Auch wenn »gelöschte« Termine bei normalen Abfragen nicht ausgegeben werden, so kann es bei bestimmten Abfragen doch passieren, dass sie wieder auftauchen. Möchten Sie sicherstellen, dass solche Termine nicht ausgegeben werden, müssen Sie zusätz- lich die Eigenschaft eventStatus abfragen. Ist hier der Wert http://Schemas. google.eom/g/2005#event.canceled enthalten, ist der Termin gelöscht. Ein nor- maler Termin enthält den Wert http://schemas.google.eom/g/2005#event. confi rmed. 260
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Nach diesem Einstieg möchte ich einen kleinen Schritt zurückgehen. Bei dem vorhergehenden Beispiel habe ich einfach nur einen Kalender ausgelesen, habe dabei aber nicht beachtet, welcher Kalender das ist. Auslesen verschiedener Kalender Haben Sie einen Account bei Google Calendar, so können Sie mehrere Kalender anlegen. Somit sind Sie in der Lage, private und geschäftliche Termine zu trennen oder beispielsweise einen eigenen Kalender für Geburtstage zu führen. Fragt man Google Calendar so ab, wie oben beschrieben, erhalten Sie den »Standard-Kalen- der«, den Google per Default für Sie anlegt. Darüber hinaus legt Google standard- mäßig aber noch einen Kalender für Geburtstage an. Um diesen abzufragen, benötigen Sie eine andere URL für den Feed. Diese URL können Sie, sobald Sie die entsprechenden Informationen haben, selbst konstruieren. Dazu müssen Sie zunächst die Informationen zu den Kalendern abfragen. Hierfür ist die Methode getCalendarl_istFeed() vorgesehen. Sie liefert Ihnen ein Objekt, in dem die Informationen zu den einzelnen Feeds enthalten sind. Ein Eintrag in diesem Feed hat einen Aufbau wie diesen: <entry> <id>http://www.google.com/calendar/feeds/default/alIcalendars/full/ user%40gmail.com</id> <publi shed>2007- 07 -11T22:10:30.257Z</publi shed> <updated>2007-07-HT21:46:35.000Z</updated> <title type="text">My Primary Calendar</title> <summary type="text">A primary calendar ....</summary> <author> <name>Coach</name> <emai1>user@gmai1.com</emai1> </author> <gCal:timezone value="America/Los_Angeles"/> <gCal:hidden value="false"/> <gCal:color value="#2952A3"/> <gCal:seiected value="true"/> <gCal :accesslevel value=''owner"/> <gd:where valueString="Mountain View"/> </entry> Interessant dabei ist das Element id. Es enthält im letzten Teil eine eindeutige Kennung für den entsprechenden Kalender. Mit dieser Kennung, die im Google- Manual auch als »UserlD« bezeichnet wird, kann man die URL des entsprechen- den Kalender-Feeds erstellen. Bei dem Hauptkalender entspricht diese ID der E-Mail-Adresse, mit der Sie sich anmelden. Leider ist zurzeit anscheinend noch 261
e-bol.net 5 | Webservices keine Methode in Zend_Gdata vorhanden, um diese ID entsprechend aufzuberei- ten, sodass dies manuell geschehen muss. Nachdem man die ID extrahiert hat, könnte man die URL manuell erstellen. Glücklicherweise ist das aber nicht nötig. Die Klasse Zend_Gdata_Calender_ EventQuery hilft Ihnen dabei. Eine solche Queiy-Klasse steht allen GData-Anwen- dungen zur Verfügung. Die Klasse ist ein Kind der Klasse Zend_Gdata_Query, wel- che die Funktionalitäten definiert, die in allen Klassen benötigt werden. Ein Objekt der Klasse Zend_Gdata_Cal ender_EventQuery erhalten Sie, wenn Sie aus dem Kalender-Objekt heraus die Methode newEventQuery() aufrufen. Damit die Methode die korrekte URL für den Feed konstruieren kann, benötigt sie mindestens die User-ID, die Visibility und den Projection-LeveL Die User-ID ist derjenige Teil, der aus der oben erwähnten ID extrahiert wird. Die Visibility, also die Sichtbarkeit, bezeichnet, ob Sie nur die Einträge erhalten wollen, die für alle sichtbar sind, oder ob Sie die Einträge haben wollen, die für Sie als Eigentü- mer des Kalenders sichtbar sind. Im ersten Fall würden Sie der Methode setVi - sibilityO 'public' übergeben und im zweiten Fall, der der übliche sein dürfte, dagegen 'private'. Der Projection-Level legt fest, wie viele Daten Sie von den Einträgen auslesen wollen. Hier dürfte es üblich sein, der Methode set- Projection() den Wert 'full' zu übergeben, um alle Daten zu erhalten. Mög- lich wäre allerdings auch der Wert ’basic'. Nachdem Sie das Objekt entspre- chend vorbereitet haben, können Sie auch schon den Kalender abfragen, indem Sie das Objekt als Parameter an die Methode getCal endarEventFeecK ) überge- ben. Das fertige Listing zum Abfragen aller Kalender könnte beispielsweise so aussehen: requi re_once 'Zend/Gdata/ClientLogin.php'; requi re_once 'Zend/Gdata/Calendar .php'; require_once 'Zend/Gdata/Calendar/EventQuery.php'; require_once ’Zend/Date.php’; header ('Content-Type: text/html; charset=utf-8’); Semail = 'zf-buch@netviser.de': Spasswd = 'total geheim*; Sservice = 'ei' ; try { $client = Zend_Gdata_ ClientLogin::getHttpClient(Semai1, Spasswd, $service): } catch (Exception $e) ( 262
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 die ('Folgender Fehler trat auf: ' . Se->getMessage()): // Auslesen der Kalender Seal = new Zend_Gdata_Calendar(Seiient): Sal1e_kalender = Seal->getCalendarListFeed(): foreach (Sal1e_kalender as Skalender) { echo *<table bgcolor="'.Skaiender->color. ; echo '<tr><td><b>Titel des Kalenders:</bX/td>': echo '<td>' .Skalender->title.'</tdX/tr>'; // User-ID extrahieren: $kal_id = $kalender->id; $user_id = substr(strrchr($kal_id, 1 ); echo '<trXtdXb>extrahierte User-ID:</bX/td>': echo * <td> ’. $user_id.'</tdX/tr>'; echo '<trXtdXb>Autor:</bX/td>'; echo '<td>'.Skaiender->author[0]->name. ’</tdX/tr>'; // Einträge im Kalender abfragen Squery = Seal->newEventQuery(); // User-ID setzen Squery->setUser(Suser_i d): // Sichtbarkeit setzen Squery->setVisibi 1ity('private'); // Umfang der Daten definieren Squery->setProjection(’ful1'): Seintraege = Seal->getCalendarEventFeed(Squery); foreach (Seintraege as Seintrag) { echo "<trXtd>Titel: </td>"; echo "<td>" .$eintrag->title."</tdX/tr>"; echo "<trXtd>Startzeit: </td>"; $dat_start = (string) new Zend_Date( Seintrag->when[O]->startTime, Zend_Date::ISO_8601, 'de_DE'): echo "<td>”. $dat_start. "</tdX/tr>"; echo "<trXtd>Endzeit: </td>": $dat_ende = (string) new Zend_Date( $eint rag->when[O]->endTime, Zend_Date::ISO_8601, 'de_DE'): echo "<td>”. $dat_ende. "</tdX/tr>"; echo "<trXtd colspn='2'>&nbsp;</tdX/tr>"; 263
e-bol.net 5 | Webservices } echo '</table>'; Listing 5.14 Ausgabe aller Kalender eines Benutzers Abbildung 5.15 Ausgabe aller Kalenderdaten Wie Sie in Abbildung 5.15 sehen, habe ich die Ausgabe der einzelnen Termine ein wenig gekürzt. Die Kalender sind in diesem Fall farblich hinterlegt. Dabei handelt es sich um dieselben Farben, die auch bei der Darstellung bei Google genutzt werden. Die Tatsache, dass bei dem Kalender »Geburtstage« kein Autor angegeben ist, liegt daran, dass ich ihn bei Google nicht eingepflegt habe. Bei dem Termin in der Geburtstagsliste handelt es sich um ein ganztägiges Ereignis, genau wie bei dem Termin »Lottoschein abgeben« im Hauptkalender. Daraus resultieren die Start- und die Endzeit. 264
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Nutzung des Event-Query-Objekts Das EventQuery-Objekt kann aber noch mehr. Die Methoden, die Sie bisher ken- nengelernt haben, sind spezifisch für die Kalenderabfragen. Allerdings sind im Query-Objekt einige Abfragen enthalten, die in allen GData-Bereichen zur Verfü- gung stehen. Das ist beispielsweise die Möglichkeit, mit setQueryO eine Abfrage, sprich eine Volltextsuche, zu definieren. Das heißt, wenn die Zeile Squery->setQuery('Zahnarzt'); ergänzt wird, wird nur der eine Termin gefunden, in dem das Wort »Zahnarzt« vorkommt. Dabei handelt es sich übrigens nicht um eine Substring-Suche. Das heißt, eine Suche nach »Zahn« würde keinen Termin zurückliefern. Eine weitere Möglichkeit, die alle Query-Objekte unterstützen, ist die maximale Anzahl der Treffer, die Sie mit der Abfrage erhalten, einzuschränken. Das können Sie mit setMaxResul ts() machen. Damit Sie durch die Gesamtzahl der Treffer blättern können, ist es natürlich auch möglich festzulegen, mit welchem Treffer Sie beginnen wollen. Dafür ist die Methode setStart!ndex() vorgesehen. Hilfreich kann es auch noch sein, das Veröffentlichungsdatum bzw. das Aktuali- sierungsdatum einzuschränken. Dafür sind die Methoden setPubl1shedMin(), setPublishedMax(), setUpdatedMin() und setUpdatedMax() vorgesehen. Sie bekommen einen Timestamp im RFC 3339-Format übergeben und schränken die Ergebnismenge entsprechend ein. Das RFC 3339-Format ist weitestgehend kom- patibel mit ISO 8601, sodass Sie auf die entsprechenden Funktionalitäten von Zend_Date zurückgreifen können. Nur für die Kalenderabfragen gilt, dass Sie die Einträge sortieren können. Stan- dardmäßig werden die Einträge nach dem Datum der letzten Veränderung sor- tiert. Mit der Methode setOrderBy() können Sie festlegen, ob nach Startzeit des Termins ’ starttime' oder nach dem Datum der letzten Änderung 1 lastmodi- f i ed ’ sortiert werden soll. Die Richtung, in der die Daten sortiert werden sollen, also absteigend oder aufsteigend, können Sie mit der Methode setSortOrder() festlegen. Dieser übergeben Sie entweder ’ascending' oder ' a ’ für eine aufstei- gende Sortierung bzw. 'descending' oder ' d ' für eine absteigende Sortierung. Darüber hinaus sind noch weitere Funktionalitäten definiert, die Sie bitte der Dokumentation der Methode Zend_Gdata_Cal endar_EventQuery entnehmen. Neue Einträge anlegen Nachdem Sie nun wissen, wie Sie Einträge auslesen, stellt sich die Frage, wie neue Einträge im Kalender angelegt werden. Auch das ist recht einfach zu erledi- gen. Zunächst benötigen Sie ein Event En try-Objekt. Dieses muss dann nur mit 265
e-bol.net 5 | Webservices den entsprechenden Informationen bestückt werden und kann dann zu Google übertragen werden. Die wichtigsten Dinge, die bei einem Termin vorhanden sein sollten, sind natür- lich der Titel des Termins, der Ort sowie die Uhrzeit. Eine kleine Beschreibung ist sicher auch noch hilfreich. Die entsprechenden Eigenschaften kennen Sie ja schon vom Auslesen der Daten. Das heißt, die Eigenschaft title, die ausgelesen wurde, muss hier wieder mit einem Wert, genauer gesagt einem Objekt belegt werden. In diesem Fall handelt es sich um ein Objekt der Klasse Zend_Gdata_ App_Extension_Title, das durch eine magische Factory namens newTitleO angelegt wird. Gleiches gilt für die Beschreibung, die der Eigenschaft content zugewiesen wird, wobei das dazugehörige Objekt durch die Methode newCon- tent() erstellt wird. Gleiches gilt grundsätzlich auch für die Eigenschaften where und when, welche die Zeit und den Ort enthalten. Allerdings ist hier zu beachten, dass die Eigenschaf- ten mit Arrays bestückt werden müssen. Die Objekte, die mit newWhen() und newWherei) abgeleitet werden, müssen somit in Form eines indizierten Array an die Eigenschaft übergeben werden. Ein komplettes Beispiel, um einen neuen Termin anzulegen, finden Sie in Listing 5.15: Seal = new Zend_Gdata_Calendar(Seiient): // Neues Event-Objekt ableiten $event= Seal->newEventEntry(); // Titel des Termins anlegen Stext = utf8_encode(’Essen im Möpken'); Stitle = Seal->newTitle(Stext); $event->title = Stitle; // Beschreibung festlegen Stext = utf8_encode(’Den guten Anzug anziehen!'); Scontent = Seal->newContent(Stext); $event->content = Scontent: // Ort festlegen Stext = utf8_encode(’Schloß Neuhaus'): Swhere = Seal->newWhere(Stext): $event->where = array(Swhere): // Zeiten festlegen Swhen = Seal->newWhen(); 266
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 // Zeiten als Zend_Date-Objekte anlegen Sstart = new Zend_Date('18.01.2008 20:00:00’,'de_DE’): Sende = new Zend_Date(’18.01.2008 22:00:00',’de_DE'); // Nach ISO konvertieren und den Eigenschaften zuweisen $when->startTime = $start->get(Zend_Date::ISO_8601); $when->endTime = $ende->get(Zend_Date::ISO_8601); $event->when = array(Swhen); // Termin speichern Seal->insertEvent($event); Listing 5.15 Anlegen eines neuen Termins Ich denke, das Listing ist recht einfach zu verstehen. Ein paar Kleinigkeiten möchte ich aber dennoch erwähnen. Zu Beginn sollten Sie immer darauf achten, dass die Daten UTF-8-codiert sind. Nutzen Sie Texte, die nicht UTF-8-codiert sind, macht sich das nicht sofort bemerkbar. Die Klasse wirft keine Exception; Google verwirft die Daten lediglich. Das heißt, es könnte unter Umständen ein Termin angelegt werden, der leer wäre. Der zweite Punkt, auf den ich hinweisen möchte, ist, dass die Namen der Fac- tory-Methoden immer den Namen der Eigenschaften mit einem vorangestellten new entsprechen. Wenn Sie dies stets bedenken, haben Sie eine gute Chance, bei der Vielzahl der Methoden den Überblick zu behalten. Der dritte und letzte Punkt ist, dass Sie die Texte, die den Eigenschaften zugewie- sen werden sollen, direkt an den Konstruktor übergeben sollten. Zwar gibt es auch Methoden, mit denen Sie die Texte setzen können, aber das führt schnell dazu, dass Sie die falsche Methode nutzen. Wollen Sie die Texte über Methoden festlegen, setzt das eine recht gute Kenntnis der Google-API voraus. Wie der Termin im Google-Kalender dargestellt wird, sehen Sie in Abbildung 5.16. Auf diese Art und Weise wird der Termin im Standard-Kalender angelegt. Möchten Sie einen anderen Kalender ansprechen, kommt das EvenQuery-Objekt wieder ins Spiel, das Sie ja schon bei den Abfragen kennengelernt haben. Bei den Abfragen wurde das Objekt komplett übergeben. Im Hintergrund wurde aber im Endeffekt nur die URL ausgelesen, die mithilfe des Objekts konstruiert wurde. Auch beim Speichern eines Termins muss die URL des entsprechenden Kalender- Feeds konstruiert werden. Die Vorgehensweise ist dabei dieselbe, nur muss hier die URL manuell mit der Methode getQueryllrl () ausgelesen und dann als zwei- ter Parameter an die Methode i nsertEvent() übergeben werden. 267
e-bol.net 5 | Webservices Coogle Kalender > «i http://www.google.com/calendar/render ▼ G ’ google calendar Qggft EW Kalender Tgxt & Ta<?g*gr Fgtgs Wertergj> Google Kalender Cz bcta zf-buch@netviser.de | Einstellungen | Hilfe | Abmelden * Öffentliche Kalender durchsuchen | Meine Kalender durchsuchen Termin einrichten • Zurück zum Kalender Speichern | Abbrechen | Löschen | Weitere Aktionen .. « Januar 2008 • S M D M D F S I 23 24 25 26 27 28 29 130 31 1 2 3 4 5 I 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 1 2 13 4 5 6 7 8 9 Was Essen im Möpken Gäste Wann Fr 18. Jan. 20:00 - Fr 18. Jan. 22:00 O Gast» hinaufüflen Wo Schloß Neuhaus Karte Kalender | Carsten Möhrke 3d Beschreibung Den guten Anzug anziebenl Gäste können F Andere einladen F Gästeliste ansehen ▼ Hinzufügen T Meine Kalender Carsten Möhrke Geburtstage Diskussionsforum für diesen Termin □ Kommentar hinzufügen ▼ Optionen Keine Einträge vorhanden. Besuchen Sie stattdessen Erinnerung Weitere Kalender Google News . KrtwEnnrefurger dngriehtet 9/10 O YSIow 1.671s Abbildung 5.16 Der neu angelegte Termin bei Google Da in diesem Beispiel nur der Geburtstagskalender als weiterer Kalender zur Ver- fügung steht, sollte der Termin auch ganztägig angelegt werden. Um das zu errei- chen, übergeben Sie die Daten von zwei aufeinanderfolgenden Tagen, wobei Sie keine Uhrzeit angeben. Einen neuen Eintrag im Geburtstagskalender anzulegen, könnte also so aussehen: // Neues Event-Objekt ableiten $event= Seal->newEventEntry(); // Titel des Termins anlegen Stext = utf8_encode(’Geburtstag von Anika'): Stitle = $cal->newTitle(Stext): $event->title = Stitle; // Zeiten für ganztägigen Termin festlegen Swhen = Seal->newWhen(); Swhen->startTime = '2008-03-25': Swhen->endTime = '2008-03-26': $event->when = array(Swhen); // Query-Objekt ableiten Squery = Seal->newEventQuery(); // User-ID des Geburtstagskalenders 268
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 $query->setUser( ’OfvJ32us2rh0b4t2ntd9sr7b9g%40group.calendar.google.com'); Squery->setVisibility('private'); Squery->setProjection('ful1'); // URL auslesen Surl = Squery->getQueryUrl(); // Termin speichern Seal->insertEvent(Sevent, Surl); Listing 5.16 Anlegen eines Geburtags Geburtstage und einige andere Termine haben eine Eigenschaft, die sie von nor- malen Terminen unterscheidet. Und zwar wiederholen sie sich jährlich. Sie kön- nen auch solche Termine anlegen. Die Vorgehensweise ist dabei allerdings ein wenig anders. Und zwar übergeben Sie die Zeiten nicht in Form eines when- Objekts, sondern in Form eines recurrence-Objekts. Leider gibt es hier noch keine schönen Zugriffsmethoden. Daher müssen Sie die Regeln für die Wieder- holung des Termins von Hand erstellen. Dabei geben Sie den Beginn des Ter- mins, das Ende des Termins und das Wiederholungsmuster an. Der Aufbau die- ses Regelwerks ist in RFC 2445 (http://www.ietf.org/rfc/rfr2445.txt) definiert. Um den Termin, wie er oben angelegt wurde, jährlich zu wiederholen, würde im obigen Code die Festlegung der when-Bedingung entfernt. Die Zeilen würden durch die folgenden ersetzt: Srecurrence = "DTSTART;VALUE=DATE:20070325\r\n" . "DTEND;VALUE=DATE:20070326\r\n" . ”RRULE:FREQ=YEARLY\r\n"; $event->recurrence = Seal->newRecurrence(Srecurrence): Hierbei definiert DTSTART den Starttermin und DTEND den Endtermin des ersten Termins. Da es sich um ein ganztägiges Ereignis handelt, wird keine Zeit angege- ben. Mit RRULE wird die Recurrence-Rule, also diejenige Regel definiert, nach welcher sich der Termin wiederholt. FREQ=Y EARLY definiert eine jährliche Wie- derholung. Sie könnten hier auch eine monatliche Wiederholung oder eine Wie- derholung zu einem bestimmten Wochentag angeben. Weitere Informationen entnehmen Sie bitte dem RFC. Verändern von Kalendereinträgen Natürlich haben Sie auch die Möglichkeit, einen Eintrag zu verändern. Auch das ist recht einfach zu bewerkstelligen. Die Vorgehensweise dabei ist, dass Sie das Objekt des entsprechenden Eintrags auslesen, die neuen Werte darin speichern und das Ganze dann wieder zurück auf den Server speichern. 269
e-bol.net 5 | Webservices So weit, so gut. Allerdings wurde in den vorhergehenden Beispielen immer ein kompletter Kalender ausgelesen. Natürlich könnte man jetzt auf die Idee kom- men mitzuzählen, welches Objekt geändert werden soll, aber das wäre recht umständlich. Sie können aber auch einzelne Einträge gezielt auslesen. Jeder Ein- trag hat, wie Sie das auch schon von den Kalender-Feeds kennen, eine eigene URL, unter der er angesprochen werden kann. Diese URL können Sie über die Eigenschaft i d auslesen. Glücklicherweise müssen Sie diese URL nicht weiter zer- legen, sondern können sie direkt verwenden. Eine solche URL können Sie an die Methode getCal endarEventEntry() übergeben, welche dann den einzelnen Ein- trag ausliest und Ihnen ein Event-Objekt zurückgibt. Dieses können Sie anschlie- ßend verändern und mithilfe der Methode save(), welche Sie aus dem Event- Objekt heraus aufrufen, wieder abspeichern: // Verbindungsaufbau etc. Seal = new Zend_Gdata_Calendar(Seiient): // URL eines Termins der aus der Eigenschaft id ausgelesen wurde SeventURL = "http://www.google.com/calendar/feeds/zf- buch%40netvi ser.de/private/full/uitntmnOqßealn930bpc93rlcg": /// Einzelnes Event auslesen Sevent = Seal->getCalendarEventEntry(SeventURL): // Neuen Titel setzen Stext = utf8_encode(’Flug nach Hamburg, nicht nach München'): $event->title = Seal->newTitle($text): // Aktualisiertes Event wieder speichern $event->save(); Listing 5.17 Ändern eines Eintrags Sie können sonst also genau so vorgehen, als würden Sie den Termin neu anle- gen. Löschen von Kalendereinträgen Das Löschen eines Eintrags im Kalender ist dem Editieren recht ähnlich. Auch in diesem Fall sollten Sie zunächst das entsprechende Eintrags-Objekt auslesen. Danach können Sie die Methode del ete() aufrufen, die den Eintrag löscht. Seal = new Zend_Gdata_Calendar(Seiient): SeventURL = "http://www.google.com/calendar/feeds/zf- buch%40netvi ser.de/private/full/uitntmnOqßealn930bpc93rlcg": Sevent = Seal->getCalendarEventEntry(SeventURL); $event->delete(); Listing 5.18 Löschen eines Eintrags 270
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 5.5.4 Nutzung von Google Spreadsheets Unter der URL http://docs.google.com bietet Google die Möglichkeit, eine Text- verarbeitung, ein Präsentationsprogramm sowie eine Tabellenkalkulation zu nut- zen. Sollten Sie keine sonderlich anspruchsvollen Büroaufgaben mit einem Office-Paket erledigen müssen, dann ist die Nutzung der Google-Angebote eine echte Alternative. Zwar sind die Möglichkeiten nicht ganz so umfangreich wie bei einem echten Office-Paket, aber der Funktionsumfang ist schon recht beein- druckend. Das Zend Framework unterstützt zurzeit nur den Zugriff auf Spreadsheets, also die Tabellenkalkulation. Innerhalb der Tabellenkalkulation können Sie verschie- dene Arbeitsmappen (die Spreadsheets) verwalten. Ein Spreadsheet beinhaltet jeweils einzelne Tabellenblätter, die Worksheets. Innerhalb eines Worksheets sind schließlich die einzelnen Felder mit den Daten zu finden. Auch hier möchte ich Ihnen empfehlen, einen Blick in die Google-Dokumenta- tion zur Spreadsheets-API zu werfen, die Sie unter der folgenden Adresse finden: http://code.google. com/apis/spreadsheets/developers_guide_protocol. h tml Der Zugriff auf die Daten erfolgt wieder über Streams, wie Sie es schon kennen- gelernt haben. Das Anlegen neuer Spreadsheets ist zurzeit noch nicht möglich. Auslesen von Spreadsheets Bevor Sie ein Spreadsheet auslesen können, ist es hilfreich zu wissen, welche Spreadsheets überhaupt zur Verfügung stehen. Die Vorgehensweise ist hier der bei dem Auslesen der Kalender sehr ähnlich. Sie können einen Feed mit allen Spreadsheet-Einträgen auslesen, indem Sie die Methode getSpreadsheetFeed() aus einem Zend_Gdata_Spreadsheets-Objekt heraus aufrufen. Das Zend_Gdata_ Spreadsheets-Objekt wird genau so abgeleitet wie das entsprechende Kalender- Objekt. Allerdings müssen Sie beim Ableiten des HTTP-Clients die Kennung cl durch wi se ersetzen. Die Methode liefert Ihnen ein Objekt der Klasse Zend_Gdata_Spreadsheets_ SpreadsheetFeed zurück, das Sie direkt an eine foreach-Schleife übergeben kön- nen. Bei jeder Iteration wird dann ein Objekt der Klasse Zend_Gdata_ Spreadsheets_SpreadsheetEntry ausgelesen. Die dahinterliegende XML-Struk- tur finden Sie in der API-Dokumentation von Google. Die wichtigsten Eigen- schaften werden auch in Listing 5.19 verwendet: requi re_once 'Zend/Gdata/ClientLogin.php'; require_once 'Zend/Gdata/Spreadsheets.php'; require_once ’Zend/Date.php’; 271
e-bol.net 5 | Webservices header ('Content-Type: text/html; charset=utf-8’): Semail = 'zf-buch@netviser.de': Spasswd = 'total geheim*; Sservi ce = 'wi se’: try { $client = Zend_Gdata_ClientLogin::getHttpClient( $email, tpasswd, $service): } catch (Exception $e) { die ('Folgender Fehler trat auf: ' . $e->getMessage()); // Spreadsheets Client ableiten Sspread = new Zend_Gdata_Spreadsheets($client); // Feed auslesen Sspreadsheets = $spread->getSpreadsheetFeed(): echo ’<table>'; foreach ($spreadsheets as $spreadheet) { echo '<tr>'; echo '<td>Titel</td>': echo '<td>'.$spreadheet->title.'</td>’; echo '</tr>'; echo '<tr>'; echo '<td>Autor</td>': echo ’<td>’.$spreadheet->author[OJ->name.’</td>' : echo '</tr>'; echo '<tr>'; echo '<td>Geändert</td>': // Datum mit Zend_Date konvertieren $datum = new Zend_Date($spreadheet->updated. Zend_Date::ISCL8601, 'de_DE'); echo '<td>'.$datum.'</td>': echo '</tr>'; echo '<tr><td colspan=''2">&nbsp;</td></tr>'; } echo '</table>’; Listing 5.19 Auslesen der verfügbaren Spreadsheets 272
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Auch hier hat Google das Prinzip der IDs verfolgt. Das heißt, jedes Spreadsheet hat eine eigene ID. Dabei handelt es sich um den letzten Teil der Eigenschaft i d, die in jedem Spreadsheet-Objekt enthalten ist. Mit dieser ID ist es dann möglich, auf die einzelnen Arbeitsblätter (Worksheets) zuzugreifen. Auch diese können in Form eines Feeds ausgelesen werden, wobei auch jedes einzelne Tabellenblatt über eine Eigenschaft i d verfügt, von der der letzte Teil eine eindeutige Kennung innerhalb der Arbeitsmappe darstellt. Mit dieser i d ist es dann wiederum mög- lich, die Feeds abzufragen, welche die Tabellenfelder beinhalten. Ein solcher Feed beinhaltet für jede Zeile ein eigenes Objekt der Klasse Zend_Gdata_ Spreadsheets_Li stEntry. Auch wenn das bereits eine Zeile darstellt, müssen Sie auf dieses Objekt zunächst die Methode getCustom() anwenden. Sie liefert Ihnen ein Array zurück, das für jedes Feld in der Zeile ein Objekt der Klasse Zend_Gdata_Spreadsheets_Extension_Custom enthält. Diese Vorgehensweise mag auf den ersten Blick ein wenig komplex erscheinen, ist aber sehr klar struk- turiert. In dem folgenden Beispiel wird ein Spreadsheet ausgelesen, das nur ein Work- sheet beinhaltet. Die ID des Spreadsheets wurde vorher ermittelt. Das Tabellen- blatt, das ausgelesen werden soll, sehen Sie in Abbildung 5.17. Abbildung 5.17 Tabellenblatt bei Google Spreadsheets Der Code, der die Daten ausliest und darstellt, sieht folgendermaßen aus: Sspread = new Zend_Gdata_Spreadsheets($client); // ID eines Spreadsheets $spreadsheetld='http://spreadsheets.google.com/feeds/spreadsheets/ 273
e-bol.net 5 | Webservices O17931546445181751748.995413063132391215' ; // Key extrahieren SspreadsheetKey = substr(strrchr(Sspreadsheetld, 1 ); // Neues Query-Objekt erstellen und mit Key initialisieren Squery = new Zend_Gdata_Spreadsheets_DocumentQuery(); Squery->setSpreadsheetKey($spreadsheetKey); // Feed mit Tabellenblättern auslesen Sworksheets = Sspread->getWorksheetFeed(Squery); // es ist nur ein Blatt => Kann direkt übernommen werden Sworksheet = $worksheets->current(); // ID extrahieren Sworksheetld = $worksheet->id; SworksheetKey = substr(strrchr(Sworksheetld, 1 ); //Neue Abfrage erstellen und mit Key initialisieren Squery = new Zend_Gdata_Spreadsheets_ListQuery(); Squery->setSpreadsheetKey($spreadsheetKey); Squery-SsetWorksheetId(SworksheetKey); // Daten auslesen SlistFeed = Sspread->getListFeedlSquery); // Titel des Blattes ausgeben echo "Inhalt des Blattes <b>".$worksheet->title.’</b><br>'; echo "Ctable border='1'>"; // Daten der ersten Zeile ausgeben SersteZeile = $1istFeed->current()->getCustom(); // Spaltenüberschriften ausgeben echo '<tr>'; foreach (SersteZeile as Sentry) { echo ’<td>’.Sentry->columnName.'</td>'; } echo ’</tr>*; // Werte zeilenweise ausgeben foreach (SlistFeed as Sentry) { echo '<tr>'; Scustom = Sentry->getCustom(); // Einzelnes Feld ausgeben foreach (Scustom as Sfeld) { echo ' <td>' ; echo $feld->text; echo '</td>'; 274
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 echo '</tr>’; } echo ’</table>'; Listing 5.20 Abfrage der Daten aus einem Tabellenblatt Wie Sie sehen, werden hier zwei unterschiedliche Query-Klassen genutzt. Das ist zum einen Zend_Gdata_Spreadsheets_DocumentQuery, um eine Abfrage auf Dokumentebene durchzuführen, sprich das Tabellenblatt zu finden. Zum ande- ren handelt es sich um die Klasse Zend_Gdata_Spreadsheets_Li stQuery, mit der auf die Zellinhalte zugegriffen werden kann. Ein wenig verwirrend ist, dass die eindeutige Kennung für die Dokumente als Key bezeichnet wird, wohingegen die Kennung für die Blätter eine ID ist, wie Sie an den beiden folgenden Zeilen sehen können, die die zweite Abfrage initialisieren: Squery->setSpreadsheetKey($spreadsheetKey); Squery->setWorksheetId(SworksheetKey): Die meisten anderen Punkte dürften klar sein. Ich möchte aber noch auf die Aus- gabe der Spaltenüberschriften eingehen, wofür diese Zeilen zuständig sind: SersteZeile = $1istFeed->current()->getCustom(); // Spaltenüberschriften ausgeben echo ’<tr>'; foreach (SersteZeile as Sentry) { echo '<td>'.$entry->columnName.'</td>'; 1 echo ’</tr>'; Nach der Abfrage enthält S1 i stFeed für jede Zeile ein Objekt. Das erste wird mit der Methode current!) ausgelesen, und auf das Ergebnis wird sofort getCus- tom() angewandt. Dadurch erhalten Sie an der Stelle ein Array mit den Daten der ersten Zeile. Erste Zeile meint bei Google Spreadsheets aber nicht die Zeile, in der Sie die Worte »Produkt«, »Einkaufspreis« und »Verkaufspreis« sehen. Die erste Zeile ist vielmehr die darunterliegende Zeile. Die Spaltenüberschriften können leider nicht so einfach und direkt ausgelesen werden. Dafür ist die Überschrift jeder Spalte aber in jedem Zellen-Objekt in der Eigenschaft col umnName enthal- ten, was ich mir hier zunutze mache. Daher wird die erste Zeile einmal vorweg ausgelesen, und die Eigenschaft col umnName wird ausgegeben. Die Ausgabe des Scripts sehen Sie in Abbildung 5.18. 275
e-bol.net 5 | Webservices Abbildung 5.18 Ausgabe der Tabellendaten Wäre in einer der Zellen eine Formel enthalten gewesen, hätte das System nur das Ergebnis der Berechnung zurückgegeben. Wie am Anfang des Kapitels bereits erwähnt, können Sie auch hier nach einzel- nen Zellinhalten suchen. Wäre bei der zweiten Abfrage setQuery('Hose') ergänzt worden, hätte die Abfrage nur diejenigen Zeilen geliefert, in denen das Wort »Hose« vorkommt. Gerade bei einer Tabellenkalkulation wäre es natürlich langweilig, wenn man lediglich eine Textsuche durchführen könnte. Daher kennt die Spreadsheet-API auch die Möglichkeit, eine »Structured Query« auszuführen. Dabei haben Sie deutlich mehr Möglichkeiten. Wollten Sie beispielsweise alle Zeilen auslesen, bei denen der Einkaufspreis kleiner als 50 ist, so würde das Ini- tialisieren des Query-Objekts so aussehen: Squery = new Zend_Gdata_Spreadsheets_ListQuery(); $query->setSpreadsheetKey($spreadsheetKey); $query->setWorksheetId(SworksheetKey): $query->setSpreadsheetQuery(’einkaufspreis < 50’): Listing 5.21 Nutzung einer Structured Query Die Methode setSpreadsheetQuery () ist also dafür zuständig, eine Structured Query zu generieren. Hier können Sie auch die Operatoren ! = und — sowie die Verknüpfungen AND und OR nutzen, wobei Sie Teilausdrücke auch klammern dür- fen. Eine solche Abfrage bezieht sich immer auf eine ganze Zeile und nicht nur auf ein Feld. Das heißt, mit der Abfrage $query->setSpreadsheetQuery(’einkaufspreis < 50 AND Verkaufspreis < 69’): erhalten Sie lediglich die Zeile mit der Jacke. Bitte beachten Sie dabei, dass die Spaltenüberschriften, die hier als Referenz genutzt werden, zwingend kleinge- schrieben werden müssen, weil alles andere in einer Exception resultiert. 276
e-bol.net Zugriff auf Google-Dienste mit Zend_Gdata | 5-5 Einfügen von neuen Zeilen Sie können auch jederzeit eine neue Zeile in eine Tabelle einfügen. Dazu benöti- gen Sie nur die IDs bzw. die Keys der Arbeitsmappe und des Tabellenblatts. Sobald Sie diese Informationen haben, können Sie direkt aus dem Zend_Gdata_ Spreadsheets-Objekt heraus die Methode insertRowO aufrufen. Sie erhält als ersten Parameter die Daten für die neue Zeile in Form eines Arrays übergeben. Die beiden IDs folgen danach als Parameter. Das Array muss so aufgebaut sein, dass der Spaltenname jeweils als Schlüssel genutzt wird und dann auf den Wert verweist, der in der Zeile eingefügt werden soll. Von daher sollten Sie jeder Spalte einen eindeutigen Namen geben. Wenn Sie dies nicht tun, vergibt das System automatisch einen Namen für die Spalte. Diesen können Sie wie oben bereits beschrieben auslesen. Daten in eine Spalte zu schreiben, die noch nicht initialisiert wurde, ist zurzeit noch nicht möglich und resultiert in einer Exception. Das Einfügen einer Zeile könnte so aussehen: Sspread = new Zend_Gdata_Spreadsheets($client); // ID eines Spreadsheets $spreadsheetld=’http://spreadsheets.google.com/feeds/spreadsheets/ O17931546445181751748.995413063132391215' : // Key extrahieren SspreadsheetKey = substr(strrchr(Sspreadsheetld, '/’), 1 ); Sworksheetld = 'http://spreadsheets.google.com/feeds/*. 'worksheets/O17931546445181751748.995413063132391215/' . 'pri vate/ful1/od6’; SworksheetKey = substrfstrrchr(Sworksheetld, '/’), 1 ); Szeile = array (’produkt'=>’Socken' , ’einkaufspreis'=>'2','Verkaufspreis'=>'5'); $spread->insertRow(Szei1e, SspreadsheetKey, SworksheetKey); Listing 5.22 Einfügen einer neuen Zeile Löschen von Zeilen Auch das Löschen einer Zeile ist kein Problem. Die Vorgehensweise entspricht der, die Sie schon beim Löschen von Kalendereinträgen kennengelernt haben. Und zwar können Sie aus einem Zeilen-Objekt heraus die Methode del ete() auf- rufen, die die Zeile entfernt. Um möglichst einfach an das entsprechende Zeilen- Objekt zu kommen, nutzen Sie die Methode getLi stEntry(), die direkt aus dem Spreadsheets-Objekt heraus aufgerufen wird. Sie bekommt die ID der Zeile (auch in diesem Fall den kompletten Inhalt der Eigenschaft i d) übergeben und liefert 277
e-bol.net 5 | Webservices die entsprechende Zeile als Objekt zurück. Danach können Sie dann delete() aufrufen, um die Zeile zu löschen. Szei1 en Id='http://spreadsheets.google.eom/feeds/li st/ol7931546445181 751748.995413063132391215/od6/pri vate/ful 1/ckd7g' ; Szeile = Sspread->getListEntry(Szei 1 enld); Szei1e->delete(); Listing 5.23 Löschen einer Zeile Aktualisieren von Zeilen Grundsätzlich ist es auch kein Problem, eine Zeile zu aktualisieren. Die Vorge- hensweise ist ähnlich wie beim Löschen. Zunächst lesen Sie das entsprechende Entry-Objekt aus. Dieses übergeben Sie dann zusammen mit einem Array mit den neuen Daten an die Methode updateRow(). Allerdings ist hierbei ein kleiner Fallstrick zu beachten: Die Methode übernimmt nur Daten, die auch in dem Array enthalten sind. Spalten, die nicht genannt werden, verlieren ihre Werte. Wenn Sie also nur einen Wert aktualisieren wollen, so sollten Sie vorher das Array mit den alten Werten belegen, damit diese nicht verlorengehen. Das könnte dann beispielsweise so aussehen: Szei1enld='.... schrecklich lange ID Sentry = Sspread->getListEntry(Szei1enld): Szeile = Sentry->getCustom(); Sdaten = array(); // Alte Daten auslesen foreach (Szeile as Szeile) { $daten[$zel1e->columnName] = Szei1e->text; } // Neue Information einfügen SdatenE’einkaufspreis'] ='1000'; // Daten speichern Sspread->updateRow(Sentry, Sdaten); Listing 5.24 Aktualisieren einer Zeile 278
e-bol.net Der vernünftige Mensch passt sich der Welt an; der unvernünftige besteht auf dem Versuch, die Welt sich anzupassen. Deshalb hängt aller Fortschritt vom unvernünftigen Menschen ab. - George Bernard Shaw 6 Arbeit mit E-Mails und Dateiformaten 6.1 E-Mails mit Zend_Mail verarbeiten Der Zugriff auf E-Mails ist häufig ein zentraler Bestandteil vieler Anwendungen. Insbesondere alle Arten von Internetangeboten, bei denen ein Benutzer einen eigenen Account besitzt, benötigen eine solche Funktionalität. Die Funktion mail() in PHP ist in diesem Zusammenhang zwar hilfreich, aber absolut unzurei- chend. So versendet sie E-Mails standardmäßig nur über den lokal installierten E- Mail-Server, was schnell dazu führt, dass E-Mails in einem Spam-Filter hängen- bleiben oder aufgrund der Netzwerkinfrastruktur erst gar nicht verschickt wer- den können. Ein weiterer Punkt ist, dass sie schwierig in der Handhabung ist, sofern Sie E-Mails mit Anhängen verschicken wollen. Darüber hinaus kann sie auch keine E-Mails abholen. Eine solche Funktionalität könnten Sie zwar über andere PHP-Funktionen implementieren, der Aufwand aber wäre recht hoch. Um Sie bei solchen Problemen zu unterstützen, wurde Zend_Mai l implementiert. Zend_Mai l unterstützt sowohl den lokalen Versand von E-Mails als auch den Ver- sand über einen externen SMTP-Server. Beim Abholen von E-Mails werden neben POP3 auch IMAP und lokale Speichermöglichkeiten wie MBox und Mail- Dir unterstützt. Bevor ich auf die Funktionalitäten im Einzelnen eingehe, möchte ich noch einen Punkt anmerken. Bei E-Mails gibt es einige Komponenten, die nur ein einziges Mal vorkommen dürfen, wohingegen andere mehrfach genutzt werden können. Alle Teile einer E-Mail, die nur ein Mal genutzt werden dürfen, können über Funktionen gesetzt werden, deren Name mit set beginnt. Elemente, die mehr- fach auftauchen dürfen, werden jeweils mit einer Methode ergänzt, die mit add anfängt. Diese kleine Eselsbrücke hilft ein wenig, den Überblick bei den vielen Funktionen zu behalten. 279
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten 6.1.1 E-Mails versenden Im einfachsten Fall können Sie eine E-Mail direkt über den lokal installierten Mail Transport Agent (MTA) verschicken. Wie schon erwähnt, kann das gerade bei Shared Webspace schnell dafür sorgen, dass die E-Mails beim Empfänger im Spam-Filter landen. Daher sollten Sie diese Vorgehensweise mit Bedacht nutzen. Lassen Sie uns mit einem einfachen Beispiel beginnen: require_once 'Zend/Mail.php'; // Objekt ableiten Smail = new Zend_Mail(); //Empfaenger Smail->addTo('empfaenger@example.com', 'Paulchen Panther'); // Absender Smail->setFrom('info@netviser.de', 'Carsten Möhrke'); // Betreff der Mail Smail->setSubject('E-Mail aus dem Internet'); // Inhalt der Mail Smail->setBodyText('Dies ist eine Test-E-Mail'); // E-Mail versenden Smail->send(); Listing 6.1 Versenden einer E-Mail über den lokalen MTA Nach dem Instantiieren des Objekts werden zuerst der Empfänger und der Absender gesetzt. Beide Methoden bekommen als ersten Parameter die jeweilige E-Mail-Adresse übergeben und an zweiter Stelle den Namen der Person, wobei dieser Parameter optional ist. Da eine E-Mail mehrere Empfänger haben kann, können Sie die Methode addTo() auch mehrfach aufrufen. Angenehm dabei ist, dass sie sich selbstständig darum kümmert, dass \n und andere Whitespaces ent- fernt werden, die eventuell zu einem Sicherheitsproblem werden könnten. Danach werden der Betreff und der Inhalt mit den Member-Funktionen setSub- jectO und setBodyText() hinzugefügt. Der eigentliche Versand erfolgt dann durch die Methode send(). Wenn Sie gerade darüber stolpern sollten, dass keine Fehlerabfrage beim Versenden der E-Mail durchgeführt wird, so gilt auch hier, dass die Methode im Falle eines Fehlers eine Exception wirft. Die Methoden geben jeweils immer eine Referenz auf das eigentliche Objekt zurück. Ein weiterer interessanter Punkt ist die Frage nach dem Zeichensatz. In dem Bei- spiel habe ich bedenkenlos Umlaute genutzt, was in diesem Fall auch kein Pro- blem darstellt. Die Klasse geht davon aus, dass die übergebenen Strings in einer ISO-8859-l-Codierung übergeben werden. 280
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 Sollten Sie einen anderen Zeichensatz bevorzugen, ist das kein Problem. Überge- ben Sie die gewünschte Codierung einfach als String an den Konstruktor, wenn Sie das Objekt ableiten. Mit der Angabe Smail = new Zend_Mai1(1utf-8’); leiten Sie also ein Mail-Objekt ab, das mit UTF-8-codierten Texten arbeitet und die E-Mail in dieser Codierung verschickt. In diesem Beispiel wurden natürlich noch nicht alle Member-Funktionen genutzt, welche die Klasse kennt. Möchten Sie bestimmten Empfängern Kopien der E-Mail zukommen lassen, so können Sie diese Empfänger mit der Methode addCcO hinzufügen. Auch diese Methode kann mehrfach aufgerufen werden und bekommt als ersten Parameter die E-Mail-Adresse des Empfängers überge- ben. Auch hier gilt, dass Sie mit dem zweiten, optionalen Parameter noch den Namen des Empfängers angeben können. Mit der Methode addBcc() können der E-Mail Empfänger für »Blind Carbon Copies« hinzugefügt werden. Also E- Mail-Adressen von Empfängern, welche die anderen Empfänger nicht sehen. Gerade dann, wenn Sie zum Beispiel einen größeren Kundenkreis erreichen wol- len, ist das eine beliebte Vorgehensweise um die Privatsphäre der Empfänger zu schützen. Allerdings akzeptiert diese Methode nur die E-Mail-Adresse als Para- meter. Einen Namen können Sie hier nicht angeben. Den »Return-Path« einer E-Mail können Sie mit setReturnPath() setzen. Bei dem Return-Path handelt es sich um eine E-Mail-Adresse, die beispielsweise dann genutzt wird, wenn der E-Mail-Server eine automatische Antwort gene- riert, weil es den Empfänger nicht gibt. Das heißt, Sie können hier eine E-Mail- Adresse angeben, die für das automatische Bounce-Handling genutzt wird. Es handelt sich also nicht um die Adresse, an die eine echte Antwort auf die E-Mail geschickt wird. Bitte beachten Sie, dass sendmai 1 dazu neigt, den Return-Path durch einen eigenen Wert zu ersetzen. Eine ganz wichtige Methode kann noch addHeader() sein. Mit dieser Member- Funktion können Sie Header in die E-Mail einfügen, die nicht durch die anderen Methoden abgedeckt sind. Falls Sie also versuchen, hiermit einen Empfänger einer Kopie in die E-Mail aufzunehmen, so wirft die Methode eine Exception. Allerdings können Sie damit alle Header aufnehmen, für die es keine speziellen Methoden gibt. Die Methode erhält dazu als ersten Parameter den Namen des Headers übergeben und als zweiten den Wert, den er bekommen soll. Mit der Angabe Smail->addHeader(’X-Mailer', 'Fynn-Mail 1.0’); 281
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten würde der Header X-Mailer mit dem Wert Fynn-Mail 1.0 hinzugefügt. Mit einem dritten Parameter, der die Werte true und false akzeptiert, können Sie noch festlegen, ob mehrere Header mit gleichem Namen zusammengefasst wer- den. Übergeben Sie true, werden die Werte zusammengefasst, was mit einem fal se (dem Default-Wert) nicht passiert. Die folgenden beiden Zeilen Smail->addHeader('X-Mailer*, 'Fynn-Mailer 1.0'); Smail->addHeader('X-Mailer*, 'German Release',true); würden in diesem Header resultieren: X-Mailer: Fynn-Mailer 1.0, German Release Diese Methode können Sie natürlich insbesondere dann gut einsetzen, wenn Sie die Dringlichkeit der E-Mail festlegen wollen. Da diese Header leider nicht wirk- lich standardisiert sind, habe ich die Header und die dazugehörigen Werte in Tabelle 6.1 zusammengestellt. Header-Name Mögliche Werte Erläuterung Importance Highr Normal oder Low Standardisiert nach RFC 2156 und RFC 2421 Pri ori ty Urgent, Normal oder Non-urgent In RFC 2156 definiert X -Pri ori ty 1 (Highest), 2 (High), 3 (Normal), 4 (Low) oder 5 (Lowest) Standard für das E-Mail-Pro- gramm Eudora X-MSMail -Priority High, Normal oder Low Standard für Microsoft-Produkte Tabelle 6.1 Header für die Dringlichkeit von E-Mails Wollen Sie eine E-Mail mit hoher Priorität versenden, empfiehlt es sich, alle vier Header mit den entsprechenden Werten zu setzen, damit sichergestellt ist, dass jeder E-Mail-Client die Dringlichkeit erkennt. Nachdem Sie die elementaren Funktionen kennengelernt haben, mit denen Sie eine E-Mail verschicken und aufbereiten können, ist zu klären, wie Sie komple- xere E-Mails versenden können. An erster Stelle ist hier das Versenden von HTML-E-Mails zu nennen, was mit Zend_Mail erfreulich einfach ist. Um einer E-Mail einen HTML-formatierten Body hinzuzufügen, übergeben Sie diesen einfach an die Methode setBody- Html (). Diese Methode akzeptiert die gleichen Parameter wie setBodyText(). Sie sollten die beiden Methoden immer parallel benutzen. Das hat den Vorteil, dass ein Client, der HTML nicht darstellen kann, dann alternativ auf die Textver- sion zurückgreifen kann. 282
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 Smail->setBodyText('Willkommen bei Zend_Mail!'): Smail->setBodyHtml(' CDOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> </head> <body> <b><u>Wi11 kommen bei Zend_Mail!</u></b> </body> </html>'); Listing 6.2 Setzen eines HTML-Bodys einer E-Mail Bitte vergessen Sie dabei nicht, dass der HTML-Anteil immer ein komplettes HTML-Dokument darstellen sollte und somit auch alle notwendigen Tags be- inhalten muss. Anhänge Möchten Sie eine E-Mail mit Anhängen versenden, zieht das bei der Nutzung der PHP-Funktion mail () einen recht großen Aufwand nach sich. Sie müssen den Dateianhang einlesen, korrekt codieren und die Boundaries setzen. Einen Groß- teil diese Aufgaben kann Zend_Mai 1 Ihnen abnehmen. Im einfachsten Fall können Sie den Dateianhang direkt mit der Methode create- Attachmenti) erzeugen. Die Methode bekommt die Binärdaten direkt in Form eines Strings übergeben. Die Daten würden dann direkt an die E-Mail angehängt. Dabei würde sich allerdings das Problem ergeben, dass die angehängte Datei noch keinen Namen hat. Die Methode gibt Ihnen aber eine Referenz auf das Zend_Mime_Part-Objekt zurück, welches genutzt wird, um den Anhang zu spei- chern. Dieses Objekt kennt eine Eigenschaft namens f i 1 ename, der Sie den Datei- namen zuweisen können: require_once 'Zend/Mail .php’; Smail = new Zend_Mail(): //Empfänger, Absender, Body etc. setzen...... // Daten aus Datei einlesen $daten_anhang = fi1e_get_contents('kapitel.doc'): // Anhang hinzufügen Sanhang = Smai1->createAttachment(Sdaten_anhang); // Dateinamen zuweisen $anhang->fi1ename='i ndex.html'; 283
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten // E-Mail versenden $mai1->send(); Listing 6.3 Anfügen eines Dateianhangs In diesem Fall wird die Datei als echter Anhang mit dem MIME-Type appl i ca- tion/octet-stream und mit der Content-Disposition attachment verschickt. Diese Eigenschaften können Sie allerdings überschreiben. Hierzu sind die Eigen- schaften type für den MIME-Type und disposition vorgesehen. Den MIME- Type können Sie direkt oder mithilfe von Konstanten zuweisen, die in der Klasse Zend_Mime definiert sind. Hier sind TYPE_OCTETSTREAM (appl ication/octet- stream), TYPE_HTML (text/html) und TYPE_TEXT (text/pl ai n) deklariert. Alterna- tiv können Sie den MIME-Type auch direkt als String zuweisen. Für die Disposi- tion sind in derselben Klasse die Konstanten DISPOSITION_I NLINE und DISPOSITION_ATTACHMENT vorgesehen, wobei der zweite Wert der Standardwert ist. Mit DISPOSITION_I NLINE wird die angehängte Datei direkt in der E-Mail dar- gestellt, sofern das möglich ist. Standardmäßig werden die Anhänge Base64-codiert verschickt, was in den meis- ten Fällen sicherlich nicht geändert werden muss. Sollte es doch einmal erfor- derlich sein, so können Sie den Wert der Eigenschaft encoding ändern. Dieser können Sie die Konstanten ENCODING_7BIT, ENCODING_8BIT, ENCODING_QUOTED- PRINTABLE und ENC0DING_BASE64 zur Verfügung stellen. Ein Punkt, bei dem Dateianhänge eine wichtige Rolle spielen, sind HTML-E- Mails. Möchten Sie nämlich eine Grafik in den HTML-Code der Seite einbinden, so muss diese Grafik auch als Anhang verschickt werden. Um die Grafik aus dem HTML-Code der Seite anzusprechen, benötigt sie eine ID. Diese kann der Eigen- schaft i d zugewiesen werden. Dieselbe ID muss dann auch als Name des Bildes im img-Tag benutzt werden. Als »Protokoll« geben Sie in diesem Fall cid: als Abkürzung für Content-ID an. Zusätzlich muss der gesamten E-Mail noch als Content-Type mul tipart/rel ated zugewiesen werden, damit der E-Mail-Client erkennt, dass die verschiedenen Teile der E-Mail miteinander in Beziehung stehen. Dazu ist in der Klasse Zend_ Mai 1 die Methode setTypef) vorgesehen, die die Konstante MUTLI PART_RELATED übergeben bekommt, welche in der Klasse Zend_Mime definiert ist. Möchten Sie zusätzlich zu der eingebundenen Grafik noch einen weiteren Anhang mit verschicken, stellt das kein Problem dar. Diesen Anhang können Sie wie gewohnt erzeugen. In Listing Listing 6.4 sehen Sie ein Beispiel, wie eine HTML-E-Mail mit eingebundener Grafik und einem weiteren Dateianhang erzeugt wird: 284
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 require_once 'Zend/Mail.php’; // Objekt ableiten Smail = new Zend_Mail(); // Content-Type korrekt setzen Smail->setType(Zend_Mime::MULTIPART_RELATED); //Empfaenger hinzufuegen Smail->addTo('cmoehrke@netviser.de', 'Carsten Möhrke'); // Absender einfuegen Smai1->setFrom('info@netviser.de', ’Paulchen Panther'); // Betreff der E-Mail setzen Smai1->setSubject('HTML-E-Mail Grafik'); // Textinhalt der Mail Smai1->setBodyText('Leider unterstützt Ihr E-Mail-Client kein HTML'): // Zufallswert fuer die ID erzeugen Sei d = mt_rand(); // HTML-Inhalt der Mail Smai1->setBodyHtml(" <!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'> Chtml> <head> </head> <body> Das hier ist das PHP-Logo: <br> <img src='cid:Scid’> </body> </html>"); // Inline-Grafik einfuegen Slogo = file_get_contents(’logo.gif'); Sanhang = Smai1->createAttachment($1ogo); $anhang->fi1ename='1ogo.gif’; Sanhang->disposition=Zend_Mime::DISPOSIT10N_INLINE ; $anhang->type='image/gi f’; $anhang->i d=$ci d; // Weiteren Dateianhang erzeugen Sdatei = fi1e_get_contents(’index.html'); Sanhang = Smai1->createAttachment(Sdatei): 285
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten $anhang->type='applicagti on/ms-word’; $anhang->filename='datei.doc’; // E-Mail versenden $mai1->send(); Listing 6.4 Versenden einer HTML-E-Mail mit Inline-Grafik und Anhang Die fertig generierte und zugestellte E-Mail sehen Sie in der nächsten Abbildung. Abbildung 6.1 Zugestellte E-Mail in Thunderbird 6.1.2 Versand über SMTP-Server Wie schon erwähnt, können Sie E-Mails auch über andere SMTP-Server und nicht nur über den lokalen MTA verschicken. Bei einem SMTP-Server, der keine Authentifikation verlangt, können Sie einfach ein Objekt der Klasse Zend_Mail_ Transport_Smtp ableiten. Der Konstruktor der Klasse bekommt dabei den Namen des SMTP-Servers als Parameter übergeben. Das so erstellte Objekt kön- nen Sie dann an die statische Methode setDefaultTransport aus der Klasse Zend_Mail übergeben. Damit legen Sie den vorher übergebenen Server als Default-Transportweg fest. Dies bietet sich immer dann an, wenn die E-Mails nur über einen Server verschickt werden sollen, was in der Regel der übliche Weg ist. Alternativ können Sie das Objekt auch der Methode send () übergeben, wenn Sie die E-Mail versenden. 286
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 Variante 1: // Klassendateien inkludieren require_once ’Zend/Mail.php’; requi re_once ’Zend/MailZTransport/Smtp.php'; // Transport-Objekt ableiten Sserver = new Zend_Mail_Transport_Smtp(’mai1.example.com'); // Server als Default festlegen Zend_Mai1::setDefaultTransport($Server); // E-Mai 1-Objekt instantiieren und E-Mail versenden Variante 2: // Klassendateien inkludieren require_once ’Zend/Mail .php’; requi re_once ’Zend/Mail/Transport/Smtp.php'; // Transport-Objekt ableiten Sserver = new Zend_Mail_Transport_Smtp(’mai1.example.com'); // Mail-Objekt ableiten Smail = new Zend_Mail(); // E-Mail aufbauen // E-Mail über Server versenden Smail->send($server); In vielen Fällen wird der SMTP-Server allerdings nach einer Authentifikation mit Benutzernamen und Passwort verlangen. Hierbei gibt es zwei Vorgehensweisen. Die erste ist »SMTP after POP« bzw. »SMTP after IMAP«. Dabei müssen Sie sich zuerst an einem POP- bzw. IMAP-Server anmelden, bevor Sie eine E-Mail über den SMTP-Server verschicken können. Dieses Verfahren ist nach wie vor bei eini- gen Providern im Einsatz, allerdings ist es nicht das übliche Verfahren. Sollte Ihr Provider dieses Verfahren nutzen, so lesen Sie bitte in Abschnitt 6.1.3 nach, wie Sie sich zum Abholen von E-Mails anmelden. Die zweite Möglichkeit ist ein direktes Anmelden am SMTP-Server, was durch die Transport-Klasse unterstützt wird. Hierbei unterstützt die Klasse die Metho- den plain, login und cram-md5. Eine dieser Methoden übergeben Sie in einem Array zusammen mit dem Benutzernamen und dem Passwort als zweiten Para- meter an den Konstruktor der Transport-Klasse. Die Schlüssel, die in dem Array genutzt werden, müssen die Namen auth, username und password haben. 287
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten require_once 'Zend/Mail.php’; requi re_once 'Zend/Mail/Transport/Smtp.php’; Sconfig = array(’auth' => 'login', 'username' => 'versender@netviser.de', ’password' => total geheim); // Objekt ableiten Stransport = new Zend_Mail_Transport_Smtp('smtp.provider.de', Sconfig); // weitere Vorgehensweise wie oben... Listing 6.5 Versand einer E-Mail über SMTP Nachdem Sie das Objekt entsprechend abgeleitet haben, können Sie fortfahren, wie Sie es bereits kennengelernt haben. Die eigentliche Authentifikation über- nimmt das Transport-Objekt dann automatisch. Optional können Sie die Übertragung der Daten auch verschlüsseln. Verschlüs- seln heißt in diesem Fall, dass die Übergabe der Daten an den SMTP-Server ver- schlüsselt wird. Dadurch wird sichergestellt, dass Benutzername und Passwort auf dem Übertragungsweg nicht ausgespäht werden können. Die eigentliche E- Mail, die der SMTP-Server verschickt, wird allerdings nicht verschlüsselt. Möchten Sie die Übertragung verschlüsseln, so können Sie dem Array noch den Schlüssel ssl hinzufügen. Mögliche Werte dabei sind tl s (Transport Layer Secu- rity) und ssl (Secure Socket Layer). Des Weiteren können Sie mit dem Schlüssel port einen alternativen Port übergeben, falls der SMTP-Server nicht die Stan- dard-Ports benutzt. Welche Verschlüsselung Ihr SMTP-Server unterstützt und welcher Port genutzt wird, erfragen Sie bitte bei Ihrem Provider oder Systemad- ministrator. 6.1.3 E-Mails abholen Mithilfe von Zend_Mai l können Sie aber nicht nur E-Mails versenden, sondern auch einlesen. E-Mails können aus einer mbox oder einem Maildir eingelesen oder von einem POP3- bzw. IMAP-Server abgeholt werden. Nachfolgend werde ich vorwiegend auf die Nutzung von POP3 und IMAP eingehen, da die Nutzung dieser Server sicher weiter verbreitet ist als die Nutzung von mbox oder Maildir. Das IMAP- und das POP3-Protokoll unterscheiden sich deutlich in den Möglich- keiten, die zur Verfügung stehen. In beiden Fällen werden die E-Mails auf einem Server abgelegt. Bei der Nutzung eines P0P3-Servers werden die E-Mails aller- dings direkt vom Client heruntergeladen. IMAP-Server sind in der Lage, E-Mails »aufzubewahren«. Das heißt, der Client kann eine Kopie der E-Mail herunterla- 288
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 den und diese dann anzeigen. Die Original-E-Mail auf dem Server kann dann als gelesen markiert oder gelöscht werden. Zusätzlich kennt IMAP auch noch die Möglichkeit, mit Ordnern zu arbeiten, in denen E-Mails abgelegt werden kön- nen. Für jede der verschiedenen Abholmöglichkeiten ist eine eigene Klasse vorgese- hen. Die Namen beginnen jeweils mit Zend_Mai l_Storage_, woran sich dann der Name des Daten-Containers, also z. B. Pop3 oder Mai 1 di r anschließt. Die Konstruktoren der Klassen bekommen die relevanten Daten jeweils in Form eines Arrays übergeben. Für die IMAP- bzw. die POP3-Klasse könnte ein solches Array beispielsweise so aufgebaut sein: Sconfig = array(’host' => 'mail.provider.de’, ’user' => 'cmoehrke@geheim.de', ’password' => 'totalgeheim', ’ssl’ => ’TLS’. ’port’ => 1120): Mit den Schlüsseln host, user und password übergeben Sie den Namen des Ser- vers sowie Benutzernamen und Passwort an die Klasse. Mit dem optionalen Schlüssel ssl können Sie festlegen, ob die Kommunikation per SSL bzw. TLS ver- schlüsselt werden soll. Mit port - auch dieser Schlüssel ist optional - können Sie noch einen anderen Port definieren, falls kein Standard-Port genutzt werden soll. Das Auslesen von E-Mails per POP3 und IMAP ist weitgehend identisch, wobei IMAP weitergehende Möglichkeiten bietet. Daher behandle ich die grundlegen- den Funktionen hier zunächst zusammen. Das Array mit den Konfigurationsdaten wird an den Konstruktor der jeweiligen Klasse, also entweder Zend_Mail_Storage_Pop3 oder Zend_Mail_Storage_Imap übergeben. Der Konstruktor baut direkt die Verbindung auf. Sollte es dabei zu einem Fehler kommen, wirft die Klasse eine Exception vom Typ Zend_Mail_ Protocol_Excepti on. Nach dem Verbindungsaufbau können Sie mithilfe der Methode countMes- sages() herausfinden, wie viele E-Mails momentan auf dem Server vorhanden sind. Jede E-Mail können Sie über ihre Ordnungsnummer, wobei die Zählung immer mit 1 beginnt, direkt ansprechen. Um eine einzelne E-Mail vom Server abzuholen, können Sie die Methode get- Message() nutzen, welche die Nummer der E-Mail als Parameter erhält. Alterna- tiv können Sie das Objekt, mit dem Sie die Verbindung aufgebaut haben, auch direkt an eine foreach-Schleife übergeben oder als Array nutzen. In allen Fällen erhalten Sie jeweils ein Objekt der Klasse Zend_Mail_Message zurück. Dieses ent- 289
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten hält die Header sowie den Betreff der E-Mail direkt als Eigenschaften. Möchten Sie einfach nur eine Liste der E-Mails ausgeben, die auf dem Server vorhanden sind, dann könnte das so aussehen wie in Listing 6.6: header ('Content-Type: text/html; charset=UTF-8’): requi re_once ’Zend/MailZStorage/Pop3.php'; // Dekodiert MIME-Header und wandelt nach UTF-8 function headerNachUtf8(Sheader) { Sreturn = ''; // Header dekodieren $a_header = imap_mime_header_decode(Sheader); // Alle Teile des Headers durchlaufen foreach (Sa_header as $header) // Zeichensatz auslesen Scharset = $header->charset: if (Scharset == ’default') { Scharset = 'I SO-8859 -1': } // Text nach UTF-8 konvertieren Sreturn .= iconv(Scharset,'UTF-8',$header->text): 1 return Sreturn; } Sconfig = array(’host' => 'mail.provider.de', 'user' => ’cmoehrke@geheim.de’, 'password' => ’totalgeheim', 'ss1' => 'tls ’): try { Smail = new Zend_Mail_Storage_Pop3(Sconfig); 1 catch (Zend_Mail_Protocol_Exception Se) { die ("Verbindungsfehler: ".Se->getMessage()); 1 try ( 290
e-bol.net E-AAails mit Zend_Mail verarbeiten | 6.1 $anzahl_mai1s = $mai1->countMessages(); echo "Anzahl der E-Mails im Postfach: $anzahl_mai1s <brXbr>"; echo "<table border=’0' cel1spacing='0' cel1padding='5'>": echo "<tr><td>Nr.</td><td>Absender</td> <td>Betreff</tdX/tr>"; // Alle E-Mails durchlaufen foreach ($mai1 as $nummer => $message) // Um die Zeilen farblich zu unterscheiden if (0 == $nummer %2) { echo ”<tr>"; 1 el se { echo "<tr bgcolor='#CCCCCC,>": 1 echo "<td>$nummer</td>": // Ausgabe der Header Sabsender = headerNachUtf8($message->from); echo "<td>".htmlentities( $absender,ENT_QUOTES.'UTF-8')."</td>"; echo "<td>".headerNachUtf8($message->subject). "</td>"; echo "</tr>"; 1 echo "</table>": 1 catch (Zend_Mail_Exception $e) { die ($e->getMessage()): 1 Listing 6.6 Auslesen von E-AAails mit Zend_AAail Die Ausgabe des Scripts finden Sie in Abbildung 6.2. Wie Sie sehen, werden die Header alle an die Funktion headerNachUtf8() über- geben. Diese Notwendigkeit resultiert daraus, dass die Header von der Klasse nicht decodiert werden. Sie werden genau so abgelegt, wie Sie aus der E-Mail ausgelesen wurden. Die Funktion headerNachUtf8() decodiert die Header und liefert sie im UTF-8-Zeichensatz zurück. 291
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Abbildung 6.2 Übersicht über die E-Mails im Browser In diesem Zusammenhang stellt sich die Frage, welche Header in einer E-Mail vorhanden sind. Dabei sei vorab erwähnt, dass die Anzahl und der Typ der Hea- der in einer E-Mail variieren können. Üblicherweise können Sie aber davon aus- gehen, dass from (der Absender) und subject (der Betreff) vorhanden sind. Ver- suchen Sie auf einen Header zuzugreifen, der nicht vorhanden ist, wirft die Klasse eine Exception. Einen Header wie del ivery-date (das Zustellungsdatum) können Sie natürlich nicht direkt als Eigenschaft nutzen, da PHP die Syntax nicht akzeptieren würde. Allerdings ist es in diesem Fall möglich die »Camel-Case- Schreibweise« zurückzugreifen. Der Bindestrich fällt weg und der erste Buch- stabe des nächsten Wortes wird groß geschrieben. Somit könnten Sie über $mes- sage->del i veryDate auf diesen Header zugreifen. Das hat allerdings den Nach- teil, dass der Name der Eigenschaft sich nun von dem Namen des Headers unterscheidet. Ich persönlich bevorzuge aber einen anderen Weg. Möchten Sie auf einen Hea- der zugreifen, der einen Bindestrich oder Ähnliches im Namen hat, so können Sie den Namen des Headers direkt an die Methode getHeaderO übergeben. Damit können Sie die Namen der Header immer konsistent nutzen. Der Header wird als derjenige Datentyp zurückgegeben, als der er gespeichert ist; üblicher- weise als String. Sollte ein Header allerdings mehrfach vorkommen, wie das bei- spielsweise bei Received der Fall ist, so erhalten Sie die Daten per Default in Form eines Arrays. Sie können als zweiten Parameter auch den String 'array' übergeben, damit die Information als Array zurückgegeben wird; und mit dem String ’string’ stellen Sie sicher, dass Sie die Daten als String erhalten. 292
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 Die Nutzung von getHeader() löst aber nicht das Problem mit der Exception. Versuchen Sie mit dieser Methode, einen nicht vorhandenen Header auszulesen, quittiert das System dies erneut mit einer Exception.1 Ein möglicher Lösungsan- satz wäre ein Konstrukt wie dieses: try { $message->getHeader(1x-priority'); 1 catch (Exception $e) I } In diesem Beispiel soll ein Header abgefragt wurden, mit dem beispielsweise das E-Mail-Programm Thunderbird die Dringlichkeit eine E-Mail festlegt. Da andere Programme dies anders machen, würde an dieser Stelle mit einiger Wahrschein- lichkeit früher oder später eine Exception geworfen. Die flexibelste Möglichkeit ist aber sicher, die Header komplett mithilfe von getHeaders() auszulesen. In dem Fall erhalten Sie alle Header, die in der E-Mail enthalten sind, in Form eines Arrays zurück. Der Name des Headers fungiert dabei als Schlüssel. Sollte ein Header mehrere Werte enthalten, so ist unter dem Schlüssel ein weiteres Array zu finden, welches indiziert ist. Im obigen Beispiel wurden nur die Header der Mails verarbeitet. Nun ist noch zu klären, wie Sie an den eigentlichen Inhalt der E-Mail gelangen. Die einfachste Lösung ist, auf die Methode getContent() zurückzugreifen. Handelt es sich um eine einfache Text-E-Mail, liefert diese Methode den Körper der E-Mail zurück. Auch hierbei gilt wieder, dass der Content exakt so zurückgegeben wird, wie er in der E-Mail enthalten ist. Das bedeutet, dass Sie selbst sicherstellen müssen, dass der Text in den korrekten Zeichensatz konvertiert wird. Wie das funktio- niert, können Sie dem nächsten Beispiel entnehmen. Der zweite mögliche Fall ist, dass es sich nicht um eine reine Text-E-Mail handelt, sondern um eine MIME-Multipart-E-Mail. In diesem Fall sind in der E-Mail meh- rere Teile enthalten, welche alle einen eigenen MIME-Type haben. Diese Vorge- hensweise wurde eingeführt, da Dateianhänge oder Ähnliches einen anderen MIME-Type besitzen als die eigentliche E-Mail. Um zu prüfen, ob eine E-Mail aus mehreren Teilen besteht, ist die Methode i sMulti part() deklariert. Die einzel- nen Teile der E-Mail können Sie dann mit der Methode getPart() abholen, wel- che die Nummer des gewünschten Teils als Parameter übergeben bekommt. 1 Dieses Verhalten ist meiner Ansicht nach deutlich übertrieben. Bitte prüfen Sie, ob die Methode sich in zukünftigen Versionen nicht anders verhält. 293
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Alternativ können Sie das Mail-Objekt in Form einer rekursiven Iterators an eine foreach-Schleife übergeben.2 Das Objekt, das Sie auf diesem Weg erhalten, kennt die Eigenschaft contentType, in welcher der MIME-Type des Teils enthalten ist. Den Inhalt der Teils können Sie dann wiederum mit getContent() auslesen. requi re_once 'Zend/MailZStorage/Pop3.php'; // Daten werden als UTF-8 ausgegeben header ('Content-Type: text/html: charset=UTF-8’): // Extrahiert den Zeichensatz aus dem Content-Type function charsetAuslesen($content_type) { preg_match("/charset *=([A:]+)/’’, $content_type, $ret): return $ret[1]; } // Konvertiert die Daten eines MIME-Headers nach UTF-8 function headerNachUtf8($header) { $return = '’; $a_header = imap_mime_header_decode($header); foreach ($a_header as $header) { $charset = $header->charset; if ($charset == ’default’) tcharset = 'I SO - 8859 -1’ : 1 $return .= iconv($charset,'UTF-8',$header->text): return $return; } $config = array('host' => 'mail.provider.de', 'user' => 'cmoehrke@geheim.de', 'password' => ’totalgeheim’, 'ssl' => 'tls'): try ( 2 Rekursive Iteratoren sind in der SPL implementiert: http://www.php.net/~helly/php/ext/spl/ 294
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 Smail = new Zend_Mail_Storage_Pop3(Sconf1g); } catch (Zend_Mail_Protocol_Exception Se) { die ("Verbindungsfehler: ".Se->getMessage()): } $mail_id = (int) S_GETE’id*]: $akt_mail = Smai1ESmail_id]: // Haben wir eine einfache Text-E-Mail? if (false === Sakt_mai1->isMultipart()) { // Es handelt sich um eine Text-E-Mail // Text kann (fast) direkt ausgegeben werden Scontent = $akt_mai1->getContent(); // Zeichensatz aus dem Header extrahieren Scharset = charsetAuslesen( $akt_mail->getHeader('content-type')); // Text nach UTF-8 konvertieren und ausgeben echo nl2br(iconv (Scharset, 'UTF-8'.Scontent)); } el se { //Es handelt sich um eine Muitipart-E-Mai 1 // Variable zum Speichern des E-Mail Textes Sbody = ''; // Zum Speichern von Links auf Anhänge $ 1 i n k s = ''; // Jeden einzelnen Part durchlaufen foreach (new Recursivelteratorlterator($akt_mai1) as Spart) { Sheaders = Spart->getHeaders(): // Handelt es sich um einen Anhang? if (false —== isset(SheadersE’content-disposition'])) //Nein, es ist ein / der Body //Gibt es schon einen Body bzw. ist der aktuelle Body // der HTML-Teil? i f (Sbody === ’' || 0 — strpos(SheadersE'content-type*1, 'text/html')) { // Zeichensatz aus dem Header extrahieren Scharset = charsetAuslesen($headersE'content-type']); 295
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten // Text nach UTF-8 konvertieren und ausgeben Sbody = iconv (Scharset, 'UTF-8',Spart->getContent()); // Wenn es ein HTML-Body ist, dann soll nur der // Teil zwischen <body> und </body> extrahiert werden if (0 —== strpos(SheadersE'content-type’],'text/html')) { preg_match("/<body[A>]*>(,*)<\/body>/si",Sbody,Smatch); Sbody=$matchEl]; } el se { // Kein HTML, dann brauchen wir Zeilenumbrüche Sbody = nl2br(Sbody); } I 1 el se // Es handelt sich um einen Anhang // Anhänge auf der Platte speichern // Daten Base64 codiert? i f (strtoiower(SheadersE'content-transfer-encoding']) — 'base64' ) { // Whitespaces ersetzen und Daten decodieren Stmp = base64_decode(preg_repl ace( "/\s/’’,'', Spart->getContent())); el se { throw new Exception('Codierung nicht unterstützt'); // Dateinamen extrahieren preg_match("/fi lename=E' \"]?( EA' \"]+)/’’, SheadersE'content-disposition'], Sdateiname); // Datei schreiben fi1e_put_contents(Sdatei nameEU,Stmp); // Eine Grafik die in der Mail dargestellt werden soll? if (true — isset(SheadersE’content-id'])) { // Content-ID aufbereiten Seid = str_replace(SheadersE’content-id’]); 296
e-bol.net E-AAails mit Zend_AAail verarbeiten | 6.1 $cid = str_replace(’>'.’',$cid); $cid = "cid:$cid"; // Content-ID durch korrekten Dateinamen ersetzen $body = str_replace($cid,$dateiname[l], $body); } el se { // Datei wird nicht eingebunden // Links zum Download erstellen $11nks.="<a href ='SdateinameEl]'>$dateiname[l]</a> " } ) // Ausgabe der E-Mail echo "<table>"; // Absender und Betreff echo "<tr><td>Absender</td>"; Sabsender = headerNachUtf8($akt_mail->from); echo "<td>$absender</td></tr>"; echo "<trXtd>Betreff</td>"; Sbetreff = headerNachl)tf8( $akt_mai 1 ->subject); echo "<td>$betreff</tdX/tr>"; // Body der E-Mail echo XtrXtd colspan=’2' bgcolor=’#eeeeee’> $body</tdX/tr>”; // Haben wir Links auf Anhänge? if(false === empty($1inks)) { // Links ausgeben echo "CtrXtd col span='2' >$1 i nks</tdX/tr>"; echo "</table>"; } Listing 6.7 Verarbeiten von E-AAails mit Anhängen Mit diesem Beispiel können E-Mails verarbeitet werden, die über Inline-Grafiken und Anhänge verfügen. Bitte beachten Sie, dass es sich wirklich nur um ein klei- nes Beispiel handelt, das zeigen soll, wie die Klasse genutzt werden kann. Dieses Beispiel ist nicht dazu gedacht, produktiv eingesetzt zu werden. Möchten Sie eine 297
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Anwendung erstellen, die E-Mails ausgeben kann, so setzen Sie sich bitte zuerst mit dem Aufbau von E-Mails auseinander. Wie eine E-Mail mit dem obigen Code dargestellt wird, sehen Sie in Abbildung 6.3. OO o http://carsten-mohrkes-computer.local/~carsten/zf-buch/Mail/4.php CJ Q u. © O O http://carsten-mohrkes-computer.local/~carst header coni Absender Carsten Möhrke Betreff Eine HTML-E-Mail mit Inlinc-Bildem und Anhang Hallo Welt:-) Das ist das PHP-Logo Schick, oder? netviser Internet Beratung e.K. Carsten Möhrke - Zend Certified Engineer Dr.-Viktoria-Steinbiß-Str. 1b - 33602 Bielefeld Tel. 0521 / 9116046 Fax: 0521 / 9116047 Mobil 0171 / 6508784 Web: www.netviser.de PHP-Funktioncn.pdf Abbildung 6.3 HTML-E-Mail mit Inline-Grafik und Anhang 6.1.4 Löschen von E-Mails Natürlich können Sie mit Zend_Mail auch E-Mails löschen. Hierzu ist die Methode removeMessagef) vorgesehen. Auch diese ist in der POP3- und in der IMAP-Klasse vorhanden, sodass Sie sie protokollübergreifend nutzen können. Allerdings gibt es hier einen Unterschied in der Handhabung. Im POP3-Protokoll kann direkt gelöscht werden, wobei das eigentliche Löschen erst nach der kor- rekten Abmeldung vom Mail-Server erfolgt. Im IMAP-Protokoll hingegen wird eine E-Mail zunächst zum Löschen markiert und danach gelöscht. Die IMAP-Vari- ante der Methode markiert die E-Mail zum Löschen und führt danach den Expunge-Befehl aus. Das hat zur Folge, dass alle E-Mails, die bereits zum Löschen markiert waren, vom Server entfernt werden und nicht nur die eine. Bei den bisher erläuterten Funktionalitäten war es ausreichend, mit der norma- len ID der E-Mail zu arbeiten. Sollte Ihr Browser sich eventuell »alte IDs« gemerkt 298
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 haben, und wurde eine E-Mail zwischendurch gelöscht, so könnte das schlimms- tenfalls dazu führen, dass die falsche E-Mail dargestellt wird. Gehen Sie beim Löschen aber genau so vor, kann es schnell passieren, dass die falsche E-Mail gelöscht wird. Um das zu verhindern, unterstützt Sie die Klasse Unique-IDs, die jede E-Mail eindeutig identifizieren kann - zumindest wenn Ihr E-Mail-Server dieses Verfahren unterstützt. Um eine Unique-ID auszulesen, können Sie die Methode getllni queId () nutzen, die die »normale« ID der E-Mail übergeben bekommt. Die so ermittelte eindeu- tige ID können Sie dann in Ihrer Anwendung nutzen, um die E-Mails eindeutig zu identifizieren. Dummerweise benötigt der Mail-Server aber eine »Sequence- Number«, sprich eine normale ID, um eine E-Mail zu löschen. Das heißt, um den Löschvorgang zu initiieren, müssen Sie auf Basis der eindeutigen ID wieder die aktuelle Sequence-ID der E-Mail ermitteln, was mit der Methode getNumberBy- Uniqueldt) geschieht. Das Script zum Anzeigen der E-Mails müsste die IDs folgendermaßen ermitteln und nutzen: // Verbindungsaufbau etc. foreach ($mai1 as Snummer => Smessage) { Sunique = Smail->getUniqueId($nummer); // Mail ausgeben // Link zum Loeschen echo "<a href=*loeschen.php?uid=".urlencode(Sunique)." ’>": echo "Diese E-Mail löschen</a>": 1 Listing 6.8 Nutzung einer Unique-ID zum Löschen Das Script, das dann den eigentlichen Löschvorgang vornimmt, müsste die Uni- que-ID entgegennehmen und wieder in die Sequence-Nummer umwandeln, um den Löschvorgang durchzuführen: // Objekt ableiten etc. Sid = Smai 1->getNumberByUniqueId(S_GET[’uid’]); Smai1->removeMessage(Sid): Listing 6.9 Löschen einer E-Mail auf Basis einer Unique-ID Ich möchte an dieser Stelle nicht unerwähnt lassen, dass es bei der Nutzung von POP3 auch eine Methode namens undeletef) gibt, die den Löschvorgang rück- 299
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten gängig macht. Das funktioniert allerdings nur so lange, wie das Script noch läuft und sich noch nicht beim Server abgemeldet hat. Daher ist eine praktikable Nut- zung dieser Methode wohl eher selten möglich. In der IMAP-Variante gibt es keine solche Methode. 6.1.5 Erweiterte Möglichkeiten von IMAP Wie schon erwähnt, unterscheiden sich das IMAP- und das POP3-Protokoll deut- lich in ihren Möglichkeiten. IMAP kennt einige sehr interessante zusätzliche Funktionalitäten. Hierbei handelt es sich zum einen um die Möglichkeit, E-Mails mit Flags zu versehen, und zum anderen um die Möglichkeit, mit Ordnern zu arbeiten. Nutzung von Flags IMAP unterstützt sechs Flags, die in Zend_Mai l durch entsprechende Konstanten repräsentiert werden. Die Konstanten, die Sie in Tabelle 6.2 finden, sind in der Klasse Zend_Mail_Storage deklariert. Konstante Bedeutung FLAG_$EEN Die Nachricht wurde angezeigt. FLAG_ANSWERED Die E-Mail wurde beantwortet. FLAG_FLAGGED Hierbei handelt es sich um eine Markierung, die frei genutzt werden kann. FLAG_DELETED Die E-Mail soll gelöscht werden. (Sie liegt im »Papierkorb«.) FLAG_DRAFT Es handelt sich um einen Entwurf einer E-Mail. FLAG_RECENT Die E-Mail ist neu. Tabelle 6.2 IMAP-Flags Möchten Sie herausfinden, ob bestimmte Flags in einer E-Mail bereits gesetzt sind, so können Sie das mit der Methode hasFl ag() überprüfen. Sie ist innerhalb der eigentlichen E-Mail-Objekte (Klasse Zend_Mail_Message) deklariert und bekommt jeweils eine der Konstanten aus Tabelle 6.2 als Parameter übergeben. Ein boolescher Wert gibt an, ob das Flag gesetzt ist. Der Server kann allerdings nicht erkennen, ob eine E-Mail bereits angezeigt wurde. Das Setzen der Flags muss also ebenfalls vom Client vorgenommen wer- den. Ihre Anwendung muss somit dafür sorgen, dass das entsprechende Flag gesetzt wird, wenn eine E-Mail gelesen oder beantwortet wurde. Das wiederum geschieht mithilfe der Methode set FI ags(). Aber Achtung: Sie ist nicht Teil der Klasse Zend_Mai l_Message, sondern in der Klasse Zend_Mai l_Storage_Imap defi- 300
e-bol.net E-AAails mit Zend_Mail verarbeiten | 6.1 niert. Das liegt daran, dass das Objekt der Message-Klasse die bereits herunterge- ladene Nachricht repräsentiert, aber keine direkte Verbindung zum Server hat. Die Methode bekommt als ersten Parameter die ID der E-Mail und als zweiten ein Array mit einer oder mehreren Flag-Konstanten übergeben. Bitte beachten Sie, dass das Recent-Flag nicht von Ihnen gesetzt werden kann. Darum kümmert der Server sich selbst, wobei das Flag allerdings nicht von allen Mail-Servern unterstützt wird. // Verbindungsaufbau etc. // Auflistung der E-Mails foreach ($mai1 as $nummer => $message) { $betreff = headerNachl)tf8($message->subject); // Wurde die Nachricht bereits gelesen? if (false === $message->hasFlag(Zend_Mail_Storage::FLAG_SEEN)) { // Nein, wurde noch nicht gelesen echo "<b>$betreff</b><br>"; el se { // Ja, wurde gelesen echo "$betreff<br>"; // Weitere Ausgaben } Listing 6.10 Auslesen von E-AAails via IAAAP Möchten Sie eine E-Mail als gelesen markieren, so können Sie das so umsetzen: // Ableiten der Objekte und Verbindundsaufbau Sid = (int)$_GET[’id']; Smail->setFlags($id, array(Zend_Mail_Storage::FLAG_SEEN)); In diesem Beispiel muss die ID natürlich per GET übergeben werden. Haben Sie E-Mails mit dem Flag FLAG_DELETED markiert, können Sie diese alle auf einmal löschen. Dazu ist im IMAP-Protokoll der Befehl EXPUNGE definiert, den das Paket unterstützt. Er ist allerdings als Methode in der Klasse Zend_Mail_ Protocol_Imap implementiert, welche im Hintergrund eingesetzt wird und den »Low-Level-Zugriff« realisiert. Sie können diese Klasse nutzen oder einfach die schon erwähnte Methode removeMessage() aufrufen, die im Hintergrund ein EXPUNGE an den Server sendet. 301
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Nun stellt sich noch die interessante Frage, wie Sie ein Flag wieder entfernen. Wie können Sie also eine bereits gelesene E-Mail wieder als »ungelesen« kenn- zeichnen? Zum jetzigen Zeitpunkt ist hierfür leider noch keine Methode vorgese- hen. Sie können aber zu einem - wie ich finde - etwas merkwürdigen Work- around greifen.3 Die Konstanten sind als Integer-Werte deklariert, sodass Sie einer Konstante also einfach ein Minus voranstellen können, um ein Flag wieder zu entfernen. Um eine E-Mail wieder als ungelesen zu markieren, können Sie die folgende Zeile nutzen: $mail->setFlags($id, array(-Zend_Mail_Storage::FLAG_SEEN)); Mit Ordnern arbeiten IMAP unterstützt Ordner. Dadurch haben Sie die Möglichkeit, neue E-Mails im Posteingang zu halten, wohingegen E-Mails, die bereits zum Löschen markiert sind, in einem »Papierkorb-Ordner« abgelegt werden können. Wenn Sie ein neues IMAP-Postfach anlegen, ist standardmäßig der Ordner INBOX, also der Posteingang vorhanden. Meist werden Ordner aber auch noch von den E-Mail-Programmen angelegt. Da es keine Standardisierung für die Namen oder Funktionalitäten der Ordner gibt, kann es auch schnell mehrere »Papierkörbe« geben, sofern Sie unterschiedliche E-Mail-Programme nutzen. Hinzu kommt das Problem, dass Ordner noch Unterordner haben können. Um diese Problematik möglichst einfach in den Griff zu bekommen, ist die Methode getFolders() deklariert. Sie ist in der Klasse Zend_Mail_Storage_Imap deklariert und liefert alle vorhandenen Ordner als Zend_Mail_Storage_Folder- Objekte zurück. Da diese Klasse das SPL-Interface Recursi velterator implemen- tiert, ist es am einfachsten, das Objekt an den Konstruktor der SPL-Klasse Recur- si velteratorlterator zu übergeben, mit welcher Sie dann über die einzelnen Ordnerobjekte iterieren können. Sollten Sie sich nicht mit der Klasse Recursi - velteratorlterator auskennen, so könnte es hilfreich sein, einen Blick in die Dokumentation zu werfen4. In der Schleife können Sie dann die Informationen über die einzelnen Ordner abrufen. Die Methode getLocal Name() liefert dabei den lokalen Namen, also nur den eigentlichen Namen des Ordners, und getGl obal Name() den globalen Namen mit vorangestellten übergeordneten Ordnern. Etwas ungewöhnlich an dieser Stelle ist vielleicht, dass Umlaute und Sonderzei- chen in den Namen als UTF-7-codierte Strings zurückgegeben werden. Hier emp- 3 In der Klasse Zend_Mail_Protocol_Imap existiert die Methode storeO, die Sie ebenfalls nutzen können. 4 http://www.php.net/~helly/php/ext/spl/ 302
e-bol.net E-AAails mit Zend_AAail verarbeiten | 6.1 fiehlt es sich, die Namen mithilfe von mb_convert_encoding() in einen anderen Zeichensatz wie zum Beispiel UTF-8 zu wandeln. // Objekt ableiten, Verbindung aufbauen etc. // Ordner auslesen $rec_iterator = $mai1->getFolders(); // Zur Nutzung in der Schleife aufbereiten Sfolders = new Recursivelteratorlterator($rec_iterator, Recursivelteratorlterator::SELF_FIRST); echo "<tt>"; // Rekursiv über alle Ordner laufen foreach ($folders as $name => $folder) { // Namen des Ordners nach UTF-8 konvertieren $name = mb_convert_encoding($name,'UTF-8','UTF7-IMAP'); // Tiefe ermitteln Stiefe = $folders->getDepth(); // Ordner einrücken wenn nötig if ($tiefe > 0) { $name = "+->$name"; $name = str_repeat('&nbsp:'. ($tiefe-l)*3).$name; // Globalen Namen als Info ausgeben echo "Sname (Globaler Name: " .$folder->getGlobalName().")<br>"; 1 echo "</tt>'’; Listing 6.11 Auslesen einer lAAAP-Ordnerstruktur Eine mögliche Ausgabe dieses Listings sehen Sie in Abbildung 6.4. n n n Q ; © Ö © IMAP CD' Google Entwürfe (Globaler Name: Entw&APw-rfe) Gelöscht (Globaler Name: Gel&APY-scht) Gesendet (Globaler Name: Gesendet) INBOX (Globaler Name: INBOX) +->wichtig (Globaler Name: INBOX/wichtig) +->vom Chef (Globaler Name: INBOX/wichtig/vom Chef) +->von Mutti (Globaler Name: INBOX/wichtig/von Mutti) Trash (Globaler Name: Trash) Abbildung 6.4 lAAAP-Ordnerstruktur 303
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Unterhalb des Ordners INBOX befindet sich der Ordner »wichtig«, welcher noch zwei Unterordner hat. Die globalen Namen wurden in diesem Beispiel zur Infor- mation ausgegeben und nicht decodiert. Wollen Sie diese produktiv nutzen, so müssen Sie sie in einen brauchbaren Zeichensatz konvertieren. Die codierte Variante des globalen Ordnernamens können Sie allerdings gut nut- zen, um in den Ordner hinein zu wechseln. Dabei sollten Sie allerdings zuvor prüfen, ob der Ordner überhaupt selektiert werden kann, was mit der Methode isSelectable() geschieht, welche aus dem Folder-Objekt heraus aufgerufen wird. Wenn Sie also die E-Mails, die in einem Ordner enthalten sind, auslesen wollen, können Sie den globalen Namen an die Methode selectFolder() übergeben. Der Methode können Sie auch direkt ein Folder-Objekt übergeben. Kennen Sie die Struktur der Ordner schon, so können Sie die Namen der Ordner übrigens auch direkt als Eigenschaften auf das Rückgabeobjekt der Methode getFoldersO anwenden. Um direkt auf den Ordner »wichtig« aus dem obigen Beispiel zuzu- greifen, könnten Sie folgendes Konstrukt nutzen: $alle_ordner = $mai1->getFolders(): echo $al 1e_ordner->INBOX->wi chti g->getGlobalName(); // Ausgabe: INBOX/wichtig Diese Syntax funktioniert natürlich nur dann, wenn die Namen der Ordner kom- patibel zur PHP-Syntax sind. Ordner, die UTF-7-Sonderzeichen oder Leerzeichen im Namen haben, können Sie so nicht ansprechen. Nach dem Wechsel in einen Ordner können Sie mit der Methode getCurrentFolder () jederzeit den globalen Namen des Ordners, in dem Sie sich gerade befinden, auslesen: Smail->selectFolder(’INBOX/wichtig/von Mutti'); $akt_folder = Smai1->getCurrentFolder(); echo "Globaler Name des aktuellen Ordners: $akt_folder"; // Ausgabe: INBOX/wichtig/von Mutti Sollten Sie noch komplexere Operationen mit Ordnern umsetzen wollen, so wer- fen Sie bitte einen Blick auf die Methoden, welche die Klasse Zend_Mail_ Storage_Fol der zur Verfügung stellt. So ist es beispielsweise auch möglich, mit create() neue Ordner anzulegen oder Ordner mit removeFol der() zu löschen. E-Mails kopieren und verschieben Der Server kann natürlich nicht wissen, in welchen Ordner eine E-Mail einsor- tiert werden soll. Neue E-Mails landen zunächst immer im Ordner INBOX. Möchten Sie die E-Mails Ihrer Mutter automatisch in den Ordner »von Mutti« verschieben, müssten Sie dies mithilfe eines Scripts implementieren. 304
e-bol.net E-Mails mit Zend_Mail verarbeiten | 6.1 Eine E-Mail können Sie mit der Methode copyMessage() in einen anderen Ord- ner kopieren. Als ersten Parameter bekommt sie die Sequence-Nummer der E- Mail und als zweiten den globalen Namen des Zielordners bzw. eine Referenz auf das Ordnerobjekt übergeben. In den meisten Fällen werden Sie eine E-Mail aber nicht nur kopieren, sondern auch verschieben wollen. Das heißt, Sie müssten die E-Mail danach mit der Methode removeMessage() löschen. Das ist dann aber lei- der diejenige Stelle, an der es ein wenig trickreich wird. Sie können die E-Mails nicht direkt in der Schleife verschieben, mit der Sie den Inhalt eines Ordners aus- lesen, da der Iterator sonst versucht, auf E-Mails zuzugreifen, die nicht mehr da sind, was in einer Exception resultiert. Das zweite Problem sind die Sequence- Nummern. Löschen Sie eine E-Mail, so erhalten die nachfolgenden E-Mails eine neue Sequence-Nummer, die um eins kleiner ist. Unterstützt Ihr Server eindeu- tige IDs, sollten Sie diese unbedingt nutzen. Ist das nicht der Fall, müssen Sie die E-Mails von hinten, also mit der größten ID beginnend, verschieben. Eine solche Implementation könnte so aussehen: $cnt = 0: foreach ($mai1 as $id => $message) { $absender = $message->from; // ist die E-Mail von Mutti? if (false !== stripos($absender,'mutti@example.com')) { // ID merken $ids[] = $id: $cnt++; } // array mit IDs umdrehen $ids = array_reverse($ids): foreach ($ids as $id) { // E-Mail in neuen Ordner kopieren $mai1->copyMessage($id, 'INBOX/wichtig/von Mutti'): // und im ursprünglichen Ordner löschen $mai1->removeMessage($id); } echo "Achtung: $cnt neue Mails von Mutti": Listing 6.12 Verschieben von E-Mails ohne Unique-ID Die hier beschriebenen Funktionen stellen keineswegs den kompletten Umfang der Klasse dar. Da der Platz in einem Buch aber leider immer beschränkt ist, 305
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten musste ich mich hier auf die wichtigsten Funktionen beschränken. In der Doku- mentation finden Sie noch weitere Hinweise. 6.2 JSON-Daten mit Zend_Json verarbeiten Client-Server-Kommunikation ist ein altbekanntes Prinzip, das auch die Basis des Internets darstellt. Im Zeitalter von Web 2.0 und von AJAX hat sich allerdings eine Neuerung ergeben. AJAX erfordert, dass Daten im Hintergrund ausge- tauscht werden können. Und das soll natürlich so schnell wie möglich passieren. Daher ist es wichtig, das Datenvolumen so gering wie möglich zu halten. Übli- cherweise setzt AJAX dabei auf XML, wie das X im Namen schon vermuten lässt. Eine Darstellung von Daten in einem XML-Format hat allerdings oft einen gro- ßen Overhead zur Folge. Eine sehr einfache Lösung ist, auf XML zu verzichten und die Daten im JSON-Format darzustellen. JSON ist die Abkürzung für »Java- Script Object Notation«. Es handelt sich um ein Datenformat, das nativ in Java- Script übernommen werden kann. Darüber hinaus unterstützen auch viele andere Sprachen inzwischen JSON, sodass Sie darüber mit anderen Sprachen Daten austauschen können. Bitte beachten Sie, dass die Daten, die Sie an JSON übergeben, immer nach UTF- 8 codiert sein sollten. Diese Klasse zu nutzen, ist sehr unproblematisch. Im Endeffekt benötigen Sie nur die beiden statischen Methoden encode() und decode(). Sollten Sie sich gerade fragen, warum es diese Klasse überhaupt gibt, wo in PHP doch schon entspre- chende Methoden zur Konvertierung existieren, dann ist das recht einfach zu erklären. Die Methoden sind erst ab PHP 5.2.0. vorhanden. Sind sie vorhanden, so greift die Klasse auch darauf zurück. Sind sie aber nicht vorhanden, so gene- riert Zend_Json eigenständig den JSON-Code. Die erste codiert die Daten und liefert einen String zurück, der direkt in Java- Script eingebunden werden kann. < ? php require_once ’Zend/Json.php’; // Daten die in JS eingebettet werden sollen Sdaten = array ('vorname' => utf8_encode('Homer'), 'nachname' => utf8_encode('Simpson')); 306
e-bol.net JSON-Daten mit Zend_Json verarbeiten I 6-2 // Daten kodieren und in einem String ablegen $json_data=Zend_Json::encode($daten); ?> <script type="text/javascript"> // Hier werden die Daten mit PHP ausgegeben // und einer JavaScript-Variable zugewiesen var name = <?php echo $json_data; ?>; seif.document.write("Daten: "); seif.document.write(name.vorname+" "); seif.document.wri te(name.nachname); </script> Listing 6.13 Ausgabe von JSON-codierten Daten Bei der Ausführung dieses Scripts erhält der Browser die nachfolgenden Daten: <script type=”text/javascript"> // Hier werden die Daten mit PHP ausgegeben // und einer JavaScript-Variable zugewiesen var name = {"vorname":"Homer","nachname":"Simpson"}; seif.document.write("Daten: "); seif.document.write(name.vorname+" "); seif.document.wri te(name.nachname): </script> O http://l.../eins.php CD W Q Q 6 Daten: Homer Simpson Abbildung 6.5 Ausgabe der JSON-Daten mit JavaScript Die Daten stehen in JavaScript also sofort nativ zur Verfügung. Sollten Sie Daten dynamisch nachladen, müssten Sie den empfangenen String, also die JSON- Daten, an die JavaScript-Funktion evalO übergeben, welche Ihnen dann ein JavaScript-Objekt zurückgibt. Möchten Sie mithilfe der Klasse JSON-Daten wieder in PHP-Datenstrukturen umwandeln, ist das ebenfalls kein Problem. Dafür ist die Methode decodet) vor- gesehen. Sie bekommt die JSON-codierten Daten als Parameter übergeben und liefert ein Objekt oder ein Array zurück. Standardmäßig liefert die Methode ein 307
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten assoziatives Array zurück, das Sie mithilfe des zweiten Parameters bestimmen können. Übergeben Sie hier Zend_Json: :TYPE_OBJECT, so liefert die Methode ein Objekt. Ohne zweiten Parameter oder mit Zend_Json:: TYPE_ARRAY erhalten Sie das schon angesprochene assoziative Array. 6.3 Generieren von PDF-Dokumenten Dynamisch PDF-Dokumente zu generieren, ist seit jeher ein interessantes Thema bei Webanwendungen. Gerade in diesem Bereich gibt es viele altgediente Klas- sen und Funktionssammlungen, wie beispielsweise FPDF. Diese erfüllen zwar alle ihren Zweck, sind oft aber etwas störrisch zu handhaben und bieten vor allem häufig nicht alle Features, die man benötigt. Zend_Pdf bietet einige Features, die viele andere Tools nicht beherrschen. Dazu gehört die Möglichkeit, PDF-Dokumente einzulesen. Bald soll es auch möglich sein, JavaScript einzubinden. Eine Möglichkeit zum Generieren von Formularen sucht man auch bei Zend_Pdf leider vergebens. Außerdem ist Zend_Pdf zum jet- zigen Zeitpunkt (Version 1.0.3) noch nicht sehr leistungsfähig. Das heißt, es gibt noch keine Möglichkeiten, um Texte zu zentrieren oder über mehrere Seiten aus- geben zu lassen. Dennoch lassen die Ansätze und die Konzeption des Pakets auf spannende zukünftige Funktionalitäten hoffen. Die Klasse Zend_Pdf unterscheidet sich in ein paar Punkten von vielen anderen PDF-Paketen. Daher möchte ich diese Punkte erläutern, um Ihnen den Einstieg zu erleichtern. Der »Nullpunkt« einer Seite, also die Stelle, an der die X- und die Y-Koordinate ihre Nullstelle haben, befindet sich hierbei links unten und nicht oben, wie es sonst oft der Fall ist. Die Koordinaten werden in Punkten, genauer gesagt in DPI angegeben. Ein DPI ist hierbei, wie in der EDV üblich, als 1/72 Inch (ca. 0,35 mm) definiert. Eine Angabe in Millimeter ist leider nicht vorgesehen. Des Weiteren werden auch nur Integer-Werte für die Angabe von Positionen unterstützt. Wenn Sie lieber mit Millimeterangaben arbeiten, könnten Sie beispielsweise eine Funktion wie die folgende zum Konvertieren nutzen: function mm2dpi ($mm) { $faktor = 25.4/72; return round($mm / $faktor); } 308
e-bol.net Generieren von PDF-Dokumenten | 6.3 Anzumerken bleibt noch, dass Zend_Pdf erstaunlicherweise keine standardisierte Methode vorsieht, um ein PDF-Dokument direkt an den Browser des Benutzers zu schicken. Die Vorgehensweise beim Generieren einer neuen Datei ist recht einfach. Zuerst wird ein neues PDF-Objekt abgeleitet. Diesem müssen dann neue Seiten hinzu- gefügt werden. Diesen Seiten wiederum können Stile zugewiesen werden, die für die Ausgabe von Texten zur Verfügung stehen. Ein wenig gewöhnungsbedürftig dürfte dabei sein, dass Sie nicht wie bei anderen Paketen die Schriftlage (z. B. kursiv) oder Schriftstärke (z. B. fett) einfach umschal- ten können. Zend_Pdf löst diese Anforderungen über unterschiedliche Schriftar- ten, wie Sie gleich sehen werden. Das folgende Listing generiert ein kleines Beispiel-PDF-Dokument: require_once 'Zend/Pdf.php' ; try { // Neues PDF-Objekt ableiten $pdf = new Zend_Pdf(); // Neue Seite generieren $page = new Zend_Pdf_Page(Zend_Pdf_Page;:SIZE_A4); // Seite in das PDF-Dokument einfügen $pdf->pages[] = $page; // Neues Stil-Objekt ableiten tstyle = new Zend_Pdf_Style(); // Font zuweisen $font = Zend_Pdf_Font::fontWithName( Zend_Pdf_Font::FONT_HELVETICA); $style->setFont($font, 32); // Stil der Seite zuweisen $page->setStyle($style); // Text in Seite ausgeben $page->drawText('Hal 10 Welt!', 10, 800); // PDF als String auslesen tstring = $pdf->render(); 1 catch (Zend_Pdf_Exception $e) { die('Fehler: '.$e->getMessage()); } // Korrekten Header an den Browser schicken 309
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten header('Content-type: appl1cation/pdf'): header('Content-Di sposition: attachment: fi1ename="datei.pdf"'); // PDF ausgeben echo $string: Listing 6.14 Erzeugen eines PDF-Dokuments mit Zend_Pdf Der Konstruktor der Klasse Zend_Pdf benötigt keine Parameter. Die Seiten wer- den einzeln generiert. Dazu muss jeweils ein neues Objekt von der Klasse Zend_ Pdf_Page instanziiert werden. Die übliche Vorgehensweise, um eine neue Seite zu generieren, besteht darin, dem Konstruktor das Seitenformat als Konstante zu übergeben. Hierzu sind in der Klasse Zend_Pdf_Page die Konstanten SIZE_A4, SIZE_A4_LANDSCAPE, SIZE_LETTER und SIZE_LETTER_LANDSCAPE vorgesehen. Die »Landscape-Varianten« sind jeweils für das Querformat der beiden Seitenformate zuständig. Nachdem die Seite fertig generiert ist, muss sie manuell in das Array pages ein- gehängt werden. Auch das mag zunächst ein wenig gewöhnungsbedürftig erscheinen, gibt Ihnen aber andererseits auch die Möglichkeit, die Seiten jeder- zeit umzusortieren oder weitere Seiten einzufügen. Bei dieser Vorgehensweise sollten Sie aber bitte nie vergessen, dass PHP 5 mit Referenzen arbeitet und Objekte nicht kopiert werden. Sofern Sie also mit $page = new Zend_Pdf_Page(Zend_Pdf_Page::SIZE_A4); $pdf->pages[] = $page; $pdf->pages[] = $page; versuchen, zwei Seiten in das Dokument einzufügen, so gelingt das zwar, aber die beiden Seiten sind absolut identisch, da im Hintergrund dasselbe Objekt refe- renziert wird. Ein Seiten-Objekt mithilfe von clone zu duplizieren, ist übrigens nicht möglich und führt zu einer Exception. Zurzeit muss jede Seite durch Instan- tiieren eines neuen Objekts einzeln generiert werden. Um Text auszugeben, sollte ein neuer Stil genutzt werden, der dann die Schrift formatiert. Auch der Stil stellt dabei ein neues Objekt dar: Sstyle = new Zend_Pdf_Style(): $font = Zend_Pdf_Font::fontWithName( Zend_Pdf_Font::FONT_HELVETICA); $style->setFont($font, 32): Listing 6.15 Nutzen eines Stil-Objekts 310
e-bol.net Generieren von PDF-Dokumenten | 6.3 Die Schriftart wird mithilfe der Methode setFont() zugewiesen. In diesem Bei- spiel erhält sie eine der 14 Schriftarten übergeben, die jeder PDF-Reader kennt. Die Zuweisung erfolgt mithilfe der statischen Methode fontWi thName(), welche den Namen der Schriftart in Form einer Konstante übergeben bekommt. Hier können Sie auf die folgenden Konstanten zugreifen: FONT_COURIER, FONT_COURIER_BOLD, FONT_COURIER_ITALIC . FONT_COURIER_ BOLD_ITALIC. FONT_TIMES. FONT_TIMES_BOLD, FONT_TIMES_ITALIC, FONT_ TIMES_BOLD_ITALIC. FONT_HELVETICA, FONT_HELVETICA_BOLD. FONT_HELVETICA_ ITALIC. FONT_HELVETICA_BOLD_ITALIC. FONT_SYMBOL und FONT_ZAPFDINGBATS. Ich denke, dass die Namen der Schriftarten für sich sprechen, sodass ich sie nicht ausführlich erläutern muss. Die BOLD-Varianten stellen jeweils einen fetten Schriftschnitt dar, und ITALIC steht für kursive Varianten.5 Sie können allerdings auch TrueType-Fonts nutzen, wie Sie gleich noch sehen werden. Die Methode setFont() bekommt als zweiten Parameter die Schriftgröße in Punkten übergeben. Der so generierte Stil wird mit setStylet) für die nächste Ausgabe selektiert. Das heißt, die Methode sorgt dafür, dass dieser Stil so lange selektiert bleibt, bis Sie einen anderen Stil wählen. Somit ist ein solcher Stil also nicht direkt an den ausgegebenen Text geknüpft. Die Reihenfolge, in der die Methoden ausgeführt werden, ist demnach entscheidend. Entscheiden Sie anschließend, einen anderen Stil zu nutzen, sind alle Texte, die bereits ausgegeben wurden, davon nicht betroffen. Wenn Sie sich ein wenig näher mit der Klasse beschäftigen, werden Sie feststel- len, dass man hier nicht unbedingt einen Stil hätte nutzen müssen. Es wäre auch möglich gewesen, die Schriftart mit der Methode setFontO festzulegen, die ebenfalls in der Klasse Zend_Pdf_Page deklariert ist. Die Nutzung von Stilen emp- finde ich aber als flexibler und bevorzuge sie daher. Die Methode drawText () ist schließlich für die Ausgabe des Textes zuständig. Sie erwartet mindestens drei Parameter. Das ist zum Ersten der eigentliche Text, der ausgegeben werden soll. Der zweite Parameter ist die X-Koordinate, also die Angabe, wie weit der Text vom linken Rand des »Blattes« entfernt sein soll. Oder anders formuliert: Wie weit er nach rechts verschoben werden soll. Der dritte Parameter definiert, wie weit der Text vom unteren Rand des Blattes entfernt sein soll, sprich wie weit er nach oben verschoben werden soll. 5 Die Schriftarten Symbol und ZapfDingbats führten zu Problemen. Bitte testen Sie vor der Nutzung, ob diese Probleme inzwischen ausgeräumt sind. 311
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Links vom Text gibt es also einen Rand mit einer Breite von 10 Punkten (ca. 3,5 mm), und bis zum unteren Rand sind es 800 Punkte, was etwa 280 mm ent- spricht. Da der Text Hallo Welt! nicht nur stets verwendet wird, um neue Dinge vorzu- stellen, sondern auch den Vorteil hat, keine Umlaute zu enthalten, war die Aus- gabe des Textes hier unproblematisch. Möchten Sie einen Text mit Umlauten oder anderen Sonderzeichen ausgeben, ist es ratsam, den Text UTF-8-codiert zu übergeben. In diesem Fall müssen Sie der Methode drawText() mit dem vierten Parameter aber noch den verwendeten Zeichensatz mitteilen: $page->drawText(utf8_encode('Hallo schöne Welt!’), 10, 800, 'utf-8'); Die Methode renderO liest das gesamte PDF-Dokument schließlich als String aus. Nachfolgend werden nur noch die Header zum Browser geschickt, und das Dokument wird anschließend direkt mit echo ausgegeben. Zwar ist an dieser Klasse sicherlich das eine oder andere ungewöhnlich, aber grundsätzlich ist sie einfach zu handhaben. Das fertige Dokument sehen Sie in Abbildung 6.6. Alternativ wäre es auch möglich gewesen, die Datei direkt auf dem Server zu speichern. Dafür ist die Methode save() deklariert, die den Dateinamen inklu- sive des Pfads übergeben bekommt und das PDF-Dokument unter diesem Namen auf der Festplatte des Servers abspeichert. Wird ein PDF-Dokument mehrfach 312
e-bol.net Generieren von PDF-Dokumenten | 6.3 heruntergeladen, so ist es sicher deutlich performanter, das Dokument nur ein Mal zu erzeugen und nachfolgend lediglich zum Download anzubieten. 6.3.1 Nutzung anderer Schriften Nachdem Sie nun einen Überblick über die Nutzung haben, möchte ich auf etwas komplexere Formatierungen eingehen. Zunächst ist zu klären, wie Sie eine Schriftart außer den vorgegebenen nutzen können. Dabei sei mir zunächst der Hinweis gestattet, dass Sie - wenn möglich - bei den vorgegebenen Schriften bleiben sollten. Das hat zwei entscheidende Vorteile: Erstens funktionieren sie überall, und zweitens müssen sie nicht in das Dokument eingebettet werden. Sollten Sie aber eine andere Schriftart benötigen, ist das grundsätzlich kein Pro- blem. Sie können einen beliebigen TrueType-Font nutzen. Sie benötigen ledig- lich die TTF-Datei, in der die Schriftart definiert ist. Dummerweise funktionieren momentan nicht alle TrueType-Dateien. Meist haben Sie aber Erfolg, wenn Sie einen echten TrueType-Font und keinen OpenType-Font nutzen. Leider können Sie das nicht unbedingt an der Dateiendung erkennen, da auch OpenType-Fonts die Endung .ttf haben können. Es gibt allerdings mehrere Programme, welche Ihnen mitteilen, um was für eine Art von Datei es sich handelt. Sollten Sie den- noch eine Font-Datei erwischen, die nicht kompatibel ist, so erkennen Sie das daran, dass das resultierende PDF-Dokument leer ist. Am besten probieren Sie die Datei vorher also aus. Der zweite Punkt bei der Nutzung einer anderen Schriftart ist die Dateigröße des PDF-Dokuments. Diese wächst unter Umständen deutlich. Das liegt daran, dass die Schriftart in das Dokument eingebettet wird. Zwar können Sie das Einbinden der Daten auch verhindern, aber das würde voraussetzen, dass die Schriftart, die Sie genutzt haben, auch auf dem Rechner installiert ist, auf dem das PDF-Doku- ment angezeigt werden soll. Um eine andere Schriftart zu nutzen, können Sie anstelle der Methode f ontWi th - Name() die Methode fontWithPath() verwenden. Als ersten Parameter überge- ben Sie ihr den Namen der Schriftart-Datei inklusive des Pfads, falls das notwen- dig ist. Als zweiten Parameter können Sie noch weitere Kostanten übergeben. Übergeben Sie hier Zend_Pdf_Font:: EMBED_DONT_EMBED, wird die Schriftart nicht mit in das PDF-Dokument eingebettet. Wie gesagt, muss die Schriftart dann aber auf dem Zielsystem verfügbar sein, damit die Darstellung korrekt ist. Eine zweite mögliche Konstante ist hier Zend_Pdf_Font:: EMBED_DONT_COMPRESS. Hiermit wird die Schriftart zwar mit in das Dokument eingefügt, aber nicht kom- primiert. Das heißt, dass die Dateigröße zwar wächst, dafür aber steigt die Kom- patibilität mit einigen PDF-Anzeigeprogrammen. Zugegebenermaßen sollte die 313
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Kompatibilität heutzutage kein Problem mehr sein. Aber das hängt natürlich auch von der Zielgruppe und der dort vorhandenen Rechnerausstattung ab. Der Parameter lässt sich mit der Konstante Zend_Pdf_Font:: EMBED_DONT_SUBSET kombinieren, indem Sie die beiden Werte mit einem Bit-Oder (|) verbinden. PDF bietet die Möglichkeit, nur eine Untermenge der Zeichen, die in einer TTF-Datei enthalten sind, einzubinden, was ja auch sinnvoll ist, da normalerweise nicht alle Zeichen benötigt werden. Mit dieser Konstante können Sie dieses Verhalten unterbinden, sodass der komplette Datenbestand der TTF-Datei eingebunden wird.6 Möchten Sie Texte fett, kursiv oder unterstrichen darstellen, so ist das zum der- zeitigen Zeitpunkt noch nicht einfach möglich. Für kursive und fette Darstellung können Sie momentan nur auf eine Datei mit einem anderen Schriftschnitt zurückgreifen. Um einen Text zu unterstreichen, müssen Sie zurzeit noch manu- ell eine Linie darunter zeichnen. Das folgende Listing generiert ein PDF-Dokument unter Verwendung verschie- dener Schriften: try { $pdf = new Zend_Pdf(): $page = new Zend_Pdf_Page(Zend_Pdf_Page::SIZE_A4); $pdf->pages[] = tpage: $font = Zend_Pdf_Font::fontWithPath('Comic Sans MS.ttf'): $page->setFont($font, 32): $page->drawText('Comic Sans MS’, 10, 800): $font = Zend_Pdf_Font::fontWithPath('Arial.ttf’): $page->setFont($font, 32): $page->drawText('Arial’, 10, 750): $font = Zend_Pdf_Font::fontWithPath('Arial Bold.ttf'): $page->setFont($font, 32): $page->drawText('Arial fett', 10, 700): tstring = $pdf->render(): I catch (Zend_Pdf_Exception $e) { die("Fehler: ".$e->getMessage()); 6 Augenscheinlich wird zurzeit aber immer die komplette Datei inkludiert, sodass diese Konstante wirkungslos ist. 3M
e-bol.net Generieren von PDF-Dokumenten | 6.3 header('Content-type: application/pdf'); header('Content-Di sposition: attachment; fi1ename="datei.pdf"'): echo $string; Listing 6.16 Nutzung verschiedener Schriften in einem PDF-Dokument Das generierte PDF-Dokument sehen Sie in Abbildung 6.7. [*| datei-7.pdf (1 Seite) Bewegen Text Auswahlen Seitenleiste Comic Sans AAS Arial Arial fett Abbildung 6.7 PDF-Dokument mit verschiedenen TrueType-Schriften Wie Sie sehen, sind die Formatierungsmöglichkeiten für Texte zurzeit noch recht beschränkt. Lediglich die Farbe können Sie ändern, wie Sie später noch sehen werden. 6.3.2 Mehrzeilige Fließtexte Zuvor möchte ich Ihnen aber noch eine Lösung für ein größeres Problem mit auf den Weg geben. Wie Sie beim Ausprobieren wahrscheinlich schon herausgefun- den haben, kann drawTextO keine Zeilenumbrüche verarbeiten oder mehrzei- lige Texte darstellen. Sicherlich wird dieses Feature noch ergänzt werden. Sollte 315
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten das aber noch nicht der Fall sein, wenn Sie die Klasse nutzen bringt Sie das nicht weiter. Daher hier ein möglicher Lösungsansatz. Die Idee ist recht einfach. Der Text, der ausgegeben werden soll, wird zunächst in einzelne Worte zerlegt. Der Text wird dabei jeweils an den Leerzeichen zwi- schen den Worten »zerschnitten«. Danach wird bei jedem einzelnen Wort für jedes Zeichen die Breite ermittelt. Die Breiten der einzelnen Zeichen werden addiert, wodurch sich die Breite des Wortes ergibt. Dabei darf nicht vergessen werden, ein Leerzeichen mit in die Berechnung einfließen zu lassen, weil dies ja beim Zerschneiden als Delimiter genutzt wurde und somit verlorengegangen ist. Die Breitenangaben der einzelnen Worte werden dann so lange addiert, bis die zulässige Breite für eine Zeile erreicht ist. Danach wird einfach eine neue Zeile initialisiert. Damit Sie den Code verstehen können, benötigen Sie noch ein paar Hintergrund- informationen. Bei einer Glyphe handelt es sich um die Information, wie ein Zei- chen dargestellt wird. Das heißt, eine Glyphe ist die konkrete Information in der Schriftartdatei, wie ein Buchstabe darzustellen ist. Anhand einer Glyphe kann man also auch erkennen, wie breit der Buchstabe dargestellt wird. Die Glyphen werden den normalen Buchstaben über einen 16 Bit-Code zugeordnet. Daher fin- det in dem Code auch eine etwas kryptische Umrechnung statt. Dann ist da noch das Problem mit der Laufweite. Der Abstand zwischen den Buchstaben bzw. Glyphen wird auch als Laufweite bezeichnet. Leider kann man die Laufweite zurzeit weder auslesen noch beeinflussen. Das heißt, man kann nur die Breite der Buchstaben ermitteln. Sollten in einer Zeile sehr viele schmale Buchstaben wie »i« oder »1« enthalten sein, so sind dort sehr viele Zwischen- räume vorhanden, weil einfach mehr Buchstaben in die Zeile passen. Da die Breite dieser Zwischenräume allerdings nicht erfasst werden kann, wird die Zeile dann deutlich breiter als der berechnete Wert. Um das zu kompensieren, wird anhand der Anzahl der Buchstaben ein Korrekturwert berechnet. Zugegebener- maßen ist der dort enthaltene Faktor geschätzt, aber zumindest scheint er gut geschätzt zu sein und sorgt für eine ordentliche Darstellung. Allerdings hat das Verfahren noch einige Schwächen. So werden beispielsweise Worte nicht getrennt oder Satzzeichen nicht erkannt. Größere Einschränkungen sind auch, dass es den Text nur auf eine Seite verteilt, und dass der Code nur ISO- 8859-1 unterstützt. Beides können Sie aber auch selbstständig erweitern. Des Weiteren ist der Code auch eher darauf ausgelegt, verständlich zu sein, und könnte deutlich schneller werden, wenn man ihn ein wenig optimierte. 316
e-bol.net Generieren von PDF-Dokumenten | 6.3 require_once 'Zend/Pdf.php' ; function zeichenßreite(Szeichen, Zend_Pdf_Styl e Sstyle) { // Schriftgröße auslesen Sgroesse = $style->getFontSize(); // Font-Objekt auslesen Sfont = Sstyle->getFont(); // Zeichen nach UTF-16 codieren Szeichen = iconv('ISO-8859-1', 'UTF-16BE', Szeichen); // In die interne Code-Darstellung konvertieren Scode = ord($zeichen{0)) << 8 | ord($zeichen{1)); // Nummer der Glyphe auslesen Sglyphe = Sfont->cmap->glyphNumberForCharacter($code); // Breite der Glyphe in Maßeinheit em ermitteln Sbreite = Sfont->widthForGlyph(Sglyphe); // Umrechnen von em nach Punkt Sbreite = ceil(Sbreite / Sfont->getUnitsPerEm() * Sgroesse); return $breite; function wortBreite(Swort, Zend_Pdf_Style Sstyle) { Sbreite = 0: for(Scnt = 0: Scnt < strien(Swort); Scnt++) { $breite = Sbreite + zeichenBreite(Swort{Scnt), Sstyle); // Korrekturfaktor um den Abstand // zwischen den Buchstaben zu kompensieren Skorrektur = strlen($wort)*0.2; return (Sbreite + Skorrektur); function erstel1eZei1 en ($text, Zend_Pdf_Style $style, Szei1enbrei te) // Text in einzelne Worte aufsplitten Swoerter = explode(' Stext); // Array für die Zeilen initialisieren Szei1 en = array(); Szei1 en CO ] = ''; 317
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten // Zähler um auf die Zeilen zuzugreifen $zei1enzehler = 0; // Speichert die Breite der aktuellen Zeile $aktuel1e_breite = 0; // Über alle Wörter laufen und Länge ermitteln foreach (twoerter as $wort) { // Leerzeichen nach Wort ergänzen $wort = $wort.' ’ ; // Breite des Wortes zzgl. Leerzeichen auslesen $wortbreite = wortBreite($wort, $style); //Passt das Wort noch in die Zeile? if (($aktuel1e_breite + $wortbreite) < $zei1enbreite) // Zeilenlänge aufaddieren taktuel1e_breite = $aktuel1e_breite + $wortbreite; // Wort anfügen $zei1en[$zei1enzehler] .= $wort; el se // Wort passte nicht mehr in die Zeile // => Neue Zeile $aktuel1e_breite = 0; $zei1enzehler++; $zei1en[$zei1enzehler] = $wort; return $zeilen; function gibFliesstextAus($text, Zend_Pdf_Page $page, $y) { $papierbreite = $page->getWidth(); $faktor = 25.4/72; $rand = 10/$faktor; // 5mm Rand // Berechnen der Zeilenbreite in Punkt $zei1enbreite = f1oor($papierbreite - $rand*2); // Style-Objekt auslesen $style = $page->getStyle(); // Zeilenhoehe manuell berechnen 318
e-bol.net Generieren von PDF-Dokumenten | 6.3 (zeilenhoehe = (style->getFontSize()+3; (zeilen = erstel1eZei1en((text,(style. (zei1enbreite); foreach ((zeilen as $zeile) { (page->drawText(utf8_encode((zeile), (rand, (y,’utf-8'); $y = (y-(zei1enhoehe; try { $pdf = new Zend_Pdf(); (page = new Zend_Pdf_Page(Zend_Pdf_Page::SIZE_A4); $pdf->pages[] = (page; (style = new Zend_Pdf_Style(); (font = Zend_Pdf_Font::fontWithName( Zend_Pdf_Font::FONT_HELVETICA); (style->setFont((font. 12); // Hier kommt der Goethe-Text (text = 'Mir träumte (page->setStyle((style); gibFliesstextAus((text. $page,800); (string = (pdf->render(); I catch (Zend_Pdf_Exception (e) { dieCFehler: ".(e->getMessage()); } header('Content-type: application/pdf'); header('Content-Di sposition: attachment; fi1ename="datei.pdf"'); echo (string; Listing 6.17 Ausgabe von Fließtext mit Zend_Pdf Der hier verwendete Text (der im Listing natürlich nicht komplett enthalten ist) stammt übrigens von Goethe. Das fertig generierte PDF-Dokument sehen Sie in Abbildung 6.8. 319
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten y daiei-22.pdf (1 Seite) Vorherige Nächste Zoomen Bewegen Text Auswahlen (Lfl) Seitenleiste Suchen nach Mir träumte neulich in der Nacht vor Pfingstsonntag, als stünde ich vor einem Spiegel und beschäftigte mich mit den neuen Sommerkleidern, welche mir die lieben Eltern auf das Fest hatten machen lassen. Der Anzug bestand, wie ihr wißt, in Schuhen von sauberem Leder, mit großen silbernen Schnallen, feinen baumwollnen Strümpfen, schwarzen Unterkleidern von Sarsche, und einem Rock von grünem Berkan mit goldnen Balletten. Die Weste dazu, von Goldstoff, war aus meines Vaters Bräutigamsweste geschnitten. Ich war frisiert und gepudert, die Locken standen mir wie Flügelchen vom Kopfe; aber ich konnte mit dem Anziehen nicht fertig werden, weil ich immer die Kleidungsstücke verwechselte, und weil mir immer das erste vom Leibe fiel, wenn ich das zweite umzunehmen gedachte. In dieser großen Verlegenheit trat ein junger schöner Mann zu mir und begrüßte mich aufs freundlichste. "Ei, seid mir willkommen!" sagte ich, "es ist mir ja gar lieb, daß ich Euch hier sehe " - "Kennt Ihr mich denn?" versetzte jener lächelnd - "Warum nicht?" war meine gleichfalls lächelnde Antwort. "Ihr seid Merkur, und ich habe Euch oft genug abgebildet gesehen." - "Das bin ich", sagte jener, "und von den Göttern mit einem wichtigen Auftrag an dich gesandt. Siehst du diese drei Äpfel?* 1 - Er reichte seine Hand her und zeigte mir drei Äpfel, die sie kaum fassen konnte, und die ebenso wundersam schön als groß waren, und zwar der eine von roter, der andere von gelber, der dritte von grüner Farbe. Abbildung 6.8 Fließtext in einem PDF-Dokument 6.3.3 Nutzung von Farben PDF-Dokumente ohne Farbe sind meist langweilig. Daher bietet Zend_Pdf auch die Möglichkeit an, mit Farben zu arbeiten. Sehr angenehm ist, dass Zend_Pdf verschiedene Farbmodelle unterstützt, sodass Sie nicht auf RGB angewiesen sind. Alle Farben haben allerdings gemeinsam, dass Sie ein Objekt der jeweiligen Farb- klasse ableiten müssen, um die Farbe nutzen zu können. Am ehesten wird Ihnen sicher der RGB-Farbraum bekannt sein, den man bei der Programmierung ja öfter trifft. Hierbei setzt sich die Zielfarbe aus den Grundfar- ben Rot, Grün und Blau zusammen. Wenn Sie ein Objekt der dafür zuständigen Klasse Zend_Pdf_Col or_Rgb ableiten, übergeben Sie diesem drei Zahlen, die defi- nieren, wie hoch die Intensität der entsprechenden Farbe sein soll. Die erste Angabe bezieht sich auf den Rot-Anteil, die zweite auf den Grün-Anteil und die letzte auf den Blau-Anteil. Die Zahlen dürfen jeweils einen Wert zwischen 0 und 1 haben, wobei 0 für 0 Prozent Farbanteil und 1 dementsprechend für 100 Pro- zent Farbanteil steht. Wollen Sie ein PDF-Dokument erzeugen, das später in einer Druckerei genutzt werden soll, so benötigen Sie Farbangaben im CMYK-Format. Hierbei werden die Farben aus den Grundfarben Cyan, Magenta, Yellow und Black gemischt. Die dafür zuständige Klasse lautet Zend_Pdf_Col or_Cmyk. Auch hierbei übergeben Sie dem Konstruktor einfach vier Zahlen, die zwischen 0 und 1 liegen dürfen womit die Intensität der einzelnen Farbkanäle definiert wird. 320
e-bol.net Generieren von PDF-Dokumenten | 6.3 Ein wenig ungewöhnlich mutet sicher die Möglichkeit an, HTML-Farbdefinitio- nen nutzen zu können, aber auch diese Möglichkeit existiert. Der Klasse Zend_ Pdf_Col or_Html können Sie eine Farbdefinition im Zahlenformat, also zum Bei- spiel '#AF12DE', oder einen Farbnamen wie 'red’, 'gray' o.Ä. übergeben. Bei den Farbnamen stellt sich die Frage, welche Namen dem System bekannt sind. Glücklicherweise dürfen Sie mehr Namen als die Namen der 16 Grundfarben nutzen, die HTML kennt. Insgesamt stehen 140 Farbnamen zur Verfügung. Dabei handelt es sich um die Farbnamen, die von allen Browsern gleich interpretiert werden. Eine Liste dieser Namen finden Sie beispielsweise unter http://www. mountaindragon.com/html/140names. htm. Die letzte Möglichkeit ist die Nutzung der Klasse Zend_Pdf_Color_GrayScale.7 Hierbei übergeben Sie dem Konstruktur nur eine Zahl zwischen 0 und 1, mit wel- cher Sie definieren, »wie grau« die Farbe sein soll. Bei 0 ergibt sich Schwarz, wohingegen 1 für Weiß steht. Wenn Sie ein entsprechendes Farbobjekt abgeleitet haben, können Sie damit definieren, welcher Teil der Darstellung diese Farbe erhalten soll. Zend_Pdf unterscheidet hierbei zwischen Füll- und Linienfarben. Die Farbe können Sie dabei entweder einem Style-Objekt zuweisen oder direkt an das Page-Objekt übergeben. In beiden Klassen sind die Methoden setFi 1 ICol or() und setLi ne- Col or() definiert, denen Sie das Farb-Objekt beim Aufruf übergeben. Um einen Text in Rot darzustellen, müssen Sie die Füllfarbe auf Rot setzen, bevor der Text ausgegeben wird. Die Linienfarbe hat bei Texten keine Auswirkung: // Page-Objekt instantiieren etc. Sstyle = new Zend_Pdf_Style(): $font = Zend_Pdf_Font::fontWithName( Zend_Pdf_Font::FONT_HELVETICA_BOLD): $style->setFont($font, 30): // Neues Farbobjekt -> 100% Rot, 0% Grün, 0% Blau $rot = new Zend_Pdf_Color_Rgb(1,0,0); // Farbe an Stil übergeben $style->setFillColor($rot); $page->setStyle($style); $page->drawText('Hallo Weit’,10,800): Listing 6.18 Ausgabe eines farbigen Textes 7 Bitte beachten Sie, dass das S von GrayScale wirklich groß zu schreiben ist. 321
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten 6.3.4 Zeichnen in PDF-Dokumenten Nachdem Sie nun schon erfahren haben, wie Sie die Farbe einer Linie bestim- men, stellt sich noch die Frage, wie Sie Linien oder auch andere Objekte zeich- nen. Hierzu sind einige Methoden deklariert, die Sie dabei unterstützen. Zurzeit sind allerdings noch nicht alle Funktionskörper mit Leben gefüllt. Das heißt, einige Methoden sind deklariert, aber noch leer. Sollten Sie also darüber stol- pern, dass noch mehr Methoden existieren als die hier besprochenen, dann liegt das daran, dass sie zum jetzigen Zeitpunkt noch nicht implementiert sind. Bevor ich zu den Zeichenfunktionen komme, möchte ich noch zwei Methoden erläutern, mit denen Sie das Aussehen der Linien beeinflussen. Das ist zunächst die Methode setLi neWidth(). Sie definiert, welche Dicke die Linien haben sol- len, die das nächste Objekt ausmachen, und bekommt die Liniendicke in Punkten übergeben. Hierbei können Sie einen Fließkommawert nutzen, sodass Sie auch eine Linienstärke von 0.1 Punkt nutzen können. Üblicherweise ist eine Linie durchgehend gezeichnet. Sie können aber auch gestrichelte Linien mithilfe von Zend_Pdf erstellen. Die zuständige Methode set- Li neDashingPattern() ist wiederum in der Page- wie auch in der Style-Klasse definiert. Wirklich schön dabei ist, dass Sie nicht auf vorgegebene Muster festge- legt sind, sondern diese selbst definieren können. Dazu übergeben Sie der Methode ein Array mit Zahlen. Jede der Zahlen definiert, wie breit ein Teilstrich sein soll, wobei sich sichtbare und unsichtbare Striche jeweils abwechseln. Über- geben Sie der Methode also das Array array(1,3), bedeutet dies, dass ein sicht- barer Strich mit 1 Punkt Länge gezeichnet wird und danach ein »unsichtbarer« Strich mit einer Länge von drei Punkten folgt. Anders formuliert: Es wird eine durchgehende Linie gezeichnet, bei der ein Abschnitt von einem Punkt farbig ist, dann kommt ein Abschnitt mit einer Länge von drei Punkten, der nicht farbig ist, dann kommt wieder ein Punkt lang Farbe usw. Dabei ist zu beachten, dass das Array »endlos« wiederholt wird. Übergeben Sie ein Array mit einer ungeraden Anzahl von Elemente, wie arrayd, 3, 5), so führt dies zu folgendem Muster (F = farbig, U = unsichtbar; 3F heißt also drei Punkte lang und farbig): 1F, 3U, 5F, 1U, 3F, 5U, 1F, 3U, 5Fusw. Optional können Sie als zweiten Parameter noch einen Offset übergeben. Damit haben Sie die Möglichkeit, die Darstellung zu verschieben. Der Aufruf Smuster = array (3. 5); $page->setLineDashingPattern($muster, 2); sorgt dafür, dass die Linie erst mit einem Abschnitt von einem Punkt anfängt, dann folgt ein unsichtbarer Abschnitt von fünf Punkten und dann wieder ein 322
e-bol.net Generieren von PDF-Dokumenten | 6.3 sichtbarer von drei Punkten. Der zweite Parameter, in diesem Fall also die Zahl 2, sorgt dafür, dass ein Teil der Linie »übersprungen« wird. Übergeben Sie der Methode die Konstante Zend_Pdf_Page:: LINE_DASHING_ SOLID als einzigen Parameter, erhalten Sie wieder eine durchgehende Linie. Nachdem ich nun so viel über Linien geschrieben habe, muss noch geklärt wer- den, wie eine Linie gezeichnet wird. Am einfachsten ist es, wenn Sie die Methode drawLi ne() nutzen, die im Page-Objekt deklariert ist. Sie bekommt die Koordina- ten des Anfangs- und des Endpunktes übergeben und verbindet diese beiden Punkte mit einer Linie. Die Punkte werden dabei jeweils mit ihrer X- und Y-Koor- dinate auf dem Blatt angegeben, sodass die Methode insgesamt vier Zahlen als Parameter übergeben bekommt. Das folgende Listing zeichnet einige Linien mit unterschiedlichen Mustern: require_once ' Zend/Pdf.php' ; try { $pdf = new Zend_Pdf(); $page = new Zend_Pdf_Page(Zend_Pdf_Page::SIZE_A4); $pdf->pages[] = $page: $page->setLineWidth(l); $page->setLineDashingPattern(array(5,10)); $page->drawLi ne( 10,190,100,190 ): $page->setLi neDashingPattern(array(10,5)); $page->drawLi ne( 10,185,100,185): $page->setLineWidth(3); $page->setLineDashingPattern(array(10,5) ,3): $page->drawLi ne( 10,180,100,180 ): $page->setLi neDashi ngPattern( Zend_Pdf_Page::LINE_DASHING_SOLID); $page->drawLi ne(10,1Z5,100,1Z5): (string = $pdf->render(); } catch (Zend_Pdf_Exception $e) { die("Fehler: ".$e->getMessage()): 323
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten header('Content-type: application/pdf'); header('Content-Di sposi tion: attachment; f i 1ename="datei. pdf'" ): echo $string; Listing 6.19 Ausgabe von Linien mit Zend_Pdf Die so generierten Linien sehen Sie in Abbildung 6.9. Abbildung 6.9 Verschiedene Linienmuster in einem PDF-Dokument Um elementare grafische Elemente zu zeichnen, sind in der Klasse auch Metho- den vorgesehen. Die Methode drawRectangle() beispielsweise zeichnet ein Rechteck. Sie bekommt die Koordinaten zweier Punkte übergeben. Diese werden als gegenüberliegende Ecken in einem Rechteck betrachtet, und das Viereck wird dann nach diesen Vorgaben gezeichnet. Auch hier werden jeweils zuerst der X- und dann der Y-Wert des entsprechenden Punktes angegeben. Der Aufruf $page->drawRectangle(20, 50, 120, 150): zeichnet ein Viereck, bei dem die vier Eckpunkte auf den Koordinaten (20,50), (120, 50), (120, 150) und (20, 150) liegen. Das Viereck würde alle Vorgaben für die Linien- und Füllfarbe sowie für die Strichdarstellung der Linien übernehmen. Das können Sie allerdings über einen weiteren Parameter beeinflussen, für den diverse Konstanten vorgesehen sind. Diese Konstanten, die in der Klasse Zend_ Pdf_Page deklariert sind, können Sie auch bei den anderen Zeichenfunktionen nutzen, die ich noch vorstellen werde. 324
e-bol.net Generieren von PDF-Dokumenten | 6.3 Konstante Bedeutung SHAPE_DRAW_FILL_AND_STROKE Objekt mit Außenlinie zeichnen und füllen (Default) SHAPE_DRAW_FILL Objekt füllen aber keine Außenlinie zeichnen SHAPE_DRAW_STROKE Nur Außenlinie entsprechend den Vorgaben zeichnen, aber nicht füllen Tabelle 6.3 Konstanten zur Beeinflussung der Zeichenfunktionen Die folgenden Zeilen geben drei Rechtecke aus, welche die hier genannten Kon- stanten visualisieren: $page->setLi neWidth(1); $page->setLineDashingPattern(array(5,10)); $page->setF111Color(new Zend_Pdf_Color_GrayScale(0.5)); $page->setLi neColor(new Zend_Pdf_Color_Rgb(0,0,0)); //Rechteck mit Default-Einstellungen $page->drawRectangle(10,40,80,110); // Nur füllen, keine Linien $page->drawRectangle(90,40,160,110, Zend_Pdf_Page::SHAPE_DRAW_FILL); // Nur Linien, nicht füllen $page->drawRectangle(1Z0,40,240,110. Zend_Pdf_Page::SHAPE_DRAW_STROKE); Sstring = $pdf->render(); Listing 6.20 Ausgabe von drei Rechtecken Die Darstellung im PDF-Dokument sehen Sie in Abbildung 6.10. Abbildung 6.10 Drei Rechtecke mit unterschiedlichen Optionen 325
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Kreise oder Ellipsen zu zeichnen, ist ebenfalls kein Problem. Die Methode draw- CircleO kann einen Kreis oder ein Kreissegment zeichnen. Sie bekommt die Koordinaten des Kreismittelpunktes sowie den gewünschten Radius in Punkten übergeben. Rufen Sie die Methode direkt auf, wird ein Kreis um Außenlinie und Füllung gezeichnet. Möchten Sie einen kompletten Kreis zeichnen, können Sie als nächsten Parameter noch eine der bereits erwähnten Konstanten angeben, um die Darstellung dementsprechend zu beeinflussen. Möchten Sie nur ein Kreissegment zeichnen, weil Sie beispielsweise ein Torten- diagramm erstellen wollen, so geben Sie nach dem Radius den Start- und den Endpunkt des Segments im Bogenmaß an. Danach haben Sie dann noch die Mög- lichkeit, eine der Konstanten anzugeben. Im folgenden Beispiel wird zunächst ein kompletter Kreis gezeichnet. Zusätzlich wird noch ein Tortendiagramm ausgegeben, das aus drei Teilen besteht. Das erste Segment entspricht 20% (= 72°), das zweite Segment umfasst 50% (= 180°) und das letzte entspricht 30%, was 108° gleicht, da ein Kreis insgesamt 360° umfasst. Um das letzte Segment ein wenig hervorzuheben, wurde es mit einem leicht ver- setzten Mittelpunkt gezeichnet: $pdf = new Zend_Pdf(): $page = new Zend_Pdf_Page(Zend_Pdf_Page::SIZE_A4); $pdf->pages[] = $page; $page->setLineWidth(0.5): $page->setFi1lColor(new Zend_Pdf_Color_GrayScal e(0.5)); $page->setl_ineColor(new Zend_Pdf_Col or_Rgb(0,0,0)): // Kompletter Kreis $page->drawCi rcle(200,200,80): // Erstes Segment $page->setFi11Color(new Zend_Pdf_Color_Rgb(1,0,0)): $page->drawCircle(370, 200, 80, deg2rad(0), deg2rad(72), Zend_Pdf_Page::SHAPE_DRAW_FILL); // Zweites Segment $page->setFi11Color(new Zend_Pdf_Color_Rgb(0,1,0)): $page->drawCi rcle(370, 200, 80, deg2rad(72), deg2rad(252), Zend_Pdf_Page::SHAPE_DRAW_FILL); // Drittes Segment $page->setFi11Color(new Zend_Pdf_Color_Rgb(0,0,1)): $page->drawCircle(375, 195, 80, deg2rad(252), deg2rad(360). 326
e-bol.net Generieren von PDF-Dokumenten | 6.3 Zend_Pdf_Page::SHAPE_DRAW_FILL); $string = $pdf->render(); Listing 6.21 Ausgabe eines Kreises und eines Tortendiagramms Abbildung 6.11 Kreis und Tortendiagramm im PDF-Dokument Sollten Sie eine Ellipse und keinen Kreis benötigen, hilft Ihnen die Methode draw- Elli pse() weiter. Sie verhält sich ähnlich wie die Kreisfunktion. Da eine Ellipse allerdings nicht rund ist, bekommt die Methode etwas andere Parameter überge- ben. Größe und Form der Ellipse werden durch ein Rechteck beschrieben. Das heißt, Sie geben im Endeffekt die Koordinaten von zwei gegenüberliegenden Punkten in einem Rechteck an. Das System zeichnet aber kein Rechteck, sondern eine Ellipse, die genau in das Rechteck passt. Die anderen Parameter sind dann wieder identisch mit denen der Kreisfunktion. Um das zu veranschaulichen, generieren die nachfolgenden Zeilen ein »gestricheltes« Rechteck mit einer innenliegenden Ellipse. Bitte beachten Sie, dass die Methoden drawRectangle() und drawEl l i pse() dabei die gleichen Koordinaten übergeben bekommen: Spage->setLineDashingPattern(array(3,2)); $page->drawRectangle(100,100,300,200, Zend_Pdf_Page::SHAPE_DRAW_STROKE); $page->setLi neDashi ngPattern(Zend_Pdf_Page::LIN E_DASHING_ SOLID): $page->drawEl1ipse( 100,100,300,200): Listing 6.22 Ausgabe eines Rechtecks und einer Ellipse 327
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Die hiermit generierte Zeichnung sehen Sie in Abbildung 6.12. n n n Vorherige Nächste j*| datei-60.pdf (1 Seite) Zoomen Abbildung 6.12 Darstellung eines Rechtecks mit innenliegender Ellipse Die letzte der echten Zeichenmethoden ist drawPolygon(), mit der Sie ein Viel- eck zeichnen können. Sie bekommt mindestens zwei Arrays übergeben, welche die Koordinaten der Eckpunkte beschreiben. Das erste Array enthält dabei die X- Werte und das zweite die Y-Werte. Die Arrays müssen identisch lang sein. Die einzelnen Werte der Arrays werden jeweils zu Paaren zusammengefasst, die die Koordinaten eines Punktes ergeben. Diese Punkte werden nacheinander mit Linien verbunden, sodass sich die Figur ergibt. Der letzte Punkt wird dabei auto- matisch wieder mit dem ersten verbunden. Als dritten Parameter können Sie eine der schon bekannten Konstanten übergeben. Diese Methode akzeptiert aber noch einen weiteren Parameter, mit dem Sie eine der beiden folgenden Konstan- ten Übergeben: FILL_METHOD_NON_ZERO_WINDING oder FILL_METHOD_EVEN_ODD. Diese definieren, wie gefüllt werden soll, wenn einige Punkte des Objekts inner- halb der Fläche liegen. Die erste Variante (die Default-Einstellung) teilt der Methode mit, dass der gesamte umschlossene Bereich gefüllt werden soll, unab- hängig davon, ob einige Punkte des Objekts eingeschlossen werden. Die zweite Konstante sorgt dafür, dass auch Punkte bzw. Linien, die vom Objekt umfasst werden, nicht ignoriert werden. Somit kann es in der gesamten Fläche noch einen Bereich geben, der nicht gefüllt ist. Das Bild in Abbildung 6.13 zeigt dies anschaulich. Die folgenden Zeilen zeichnen zwei identische Polygone, die mit unterschiedli- chen Methoden gefüllt werden: 328
e-bol.net Generieren von PDF-Dokumenten | 6.3 $x = array (100, 400, 400, 150, 350, 100 ); $y = array (450, 450, 750, 500, 500, 750); $page->drawPolygon($x, $y, Zend_Pdf_Page::SHAPE_DRAW_FILL_AND_STROKE, Zend_Pdf_Page::FILL_METHOD_NON_ZERO_WINDING); $x = array (100, 400, 400, 150, 350, 100 ); $y = array (100, 100, 400, 150, 150, 400); $page->drawPolygon($x, $y, Zend_Pdf_Page::SHAPE_DRAW_FILL_AND_STROKE, Zend_Pdf_Page::FILL_METHOD_EVEN_ODD ); Listing 6.23 Ausgabe von zwei Polygonen mit unterschiedlichen Füllmethoden [3 datei-68.pdf (1 Seite) Vorherige Nächste Zoomen Abbildung 6.13 Zwei Polygone mit unterschiedlichen Füllmethoden 329
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten 6.3.5 Einbinden von Bildern Eine weitere Methode, die auch zu den Zeichenmethoden gehört, aber nicht wirklich zum Zeichnen dient, ist drawImageO. Mit ihr können Sie ein Bild auf einer Seite platzieren. Dazu müssen Sie das Bild aber zunächst in ein Zend_Pdf_ Image-Objekt überführen, was so funktioniert: $bi1d = Zend_Pdf_Image::1mageWithPath('bild.jpg’): Sie rufen die statische Methode imageWi thPatht) auf und übergeben ihr den Namen des Bildes inklusive des Pfads. Bei der Methode handelt es sich um eine Factoiy, die eine entsprechende Klasse einbindet, die die Bilddatei verarbeitet. Zurzeit werden Bilder im TIF-, JPG- und PNG-Format unterstützt. Ein wenig kurios mag anmuten, dass die Zuordnung der Formate anhand der Dateiendung erfolgt.8 Bitte stellen Sie also sicher, dass die Dateien korrekt benannt sind. Sollte die Dateiendung nicht das korrekte Format widerspiegeln, oder sollte eine unbe- kannte Dateiendung genutzt werden, resultiert daraus eine Exception.9 Zulässige Suffixe sind: ti f, ti ff, png, jpg, jpe und jpeg. Sobald Sie das Bild-Objekt haben, können Sie es an die Methode drawlmagel) übergeben. Als weitere Parameter folgen die X- und Y-Koordinate der unteren linken sowie die X- und Y-Koordinate der oberen rechten Ecke des Bereichs, in dem das Bild dargestellt werden soll. Das Bild wird bei der Darstellung an diesen Bereich angepasst, es wird also skaliert und gegebenenfalls auch verzerrt. Möch- ten Sie verhindern, dass das Bild verzerrt wird, können Sie die Originalgröße des Bildes mithilfe der Methoden getPi xel Hei ght () und getPi xel Wi dth() auslesen. Damit können Sie auch eine bestimmte Auflösung in DPI errechnen. Im folgen- den Beispiel wird ein »Powerd By Zend Framework« in ein PDF-Dokument ein- gebunden: // Bild einlesen $bi1d = Zend_Pdf_Image::imageWithPath('powered.png'); // Hoehe und Breite auslesen Shoehe = $bild->getPixelHeight(): Sbreite = $bi1d->getPixe1Width(); // Bild ausgeben $page->drawlmage($biId, 10, 800, 10+$breite, 800+$hoehe): Sstring = $pdf->render(); Listing 6.24 Einbinden eines Bildes mit Zend_Pdf 8 Ein Kommentar im Quelltext besagt, dass dies geändert werden soll. 9 Im Quelltext findet sich ein Hinweis, dass die Methode bei einem ungültigen Bild null zurück- gibt. Allerdings scheint die Methode bei Problemen immer eine Exception zu werfen. Den- noch kann es nicht schaden, den Rückgabewert auf null hin zu prüfen. 330
e-bol.net Generieren von PDF-Dokumenten | 6.3 Abbildung 6.14 Darstellung eines Bildes in einem PDF-Dokument Eine weitere Funktion, welche die Klasse unterstützt, ist das Clipping. Damit haben Sie die Möglichkeit, die Ausgabe von Zeichnungen oder Grafiken auf bestimmte Bereiche zu beschränken. Zu diesem Thema möchte ich Sie allerdings gerne auf die Dokumentation verweisen, da diese Funktionalität sicher eher für wenige Anwender interessant ist. 6.3.6 Meta-Informationen einfügen PDF-Dokumente unterstützen die Möglichkeit, Meta-Informationen im Doku- ment abzuspeichern. Mit anderen Worten: Sie können im Dokument hinterle- gen, wer der Autor ist, wann das Dokument erstellt wurde und Ähnliches. Diese Informationen sind in einem Array hinterlegt, das direkt im Zend_Pdf-Objekt abgelegt ist. Möchten Sie den Namen des Autors sowie einen Titel hinterlegen, so könnten Sie das so umsetzen: $pdf = new Zend_Pdf(); $pdf->properties['Author'] = 'Homer Simpson’; $pdf->properties['Title'] = 'Sicherheit in Atomkraftwerken'; Listing 6.25 Einfügen von AAeta-lnformationen Welche Schlüssel insgesamt zur Verfügung stehen, können Sie der Tabelle 6.4 entnehmen. 331
e-bol.net 6 | Arbeit mit E-Mails und Dateiformaten Schlüssel Bedeutung Author Autor des Dokuments Title Titel des Dokuments Subject kurze Inhaltsangabe Keywords Schlüsselwörter zum Inhalt des Dokuments Creator Software, mit der das ursprüngliche Dokument erstellt wurde (z. B. Microsoft Word) Producer Software, die das PDF-Dokument erstellt hat (also beispielsweise Zend_Pdf) CreationDate Zeitpunkt der Erstellung ModDate Zeitpunkt der letzten Modifikation Tabelle 6.4 Schlüssel für die Arbeit mit Meta-Informationen Die Datumsfelder CreationDate und ModDate erwarten das Datum in einem bestimmten Format. Dieses Format ist folgendermaßen aufgebaut: D:YYYYMMD- DHHmmSSOHH' mm' - das D: wird direkt übernommen. Die einzelnen Platzhalter des Beispiel-Strings haben die folgenden Bedeutungen: Platzhalter Bedeutung YYYY Jahr, vierstellig MM Monat, zweistellig DD Tag, zweistellig HH Stunde im 24-Stunden Format, zweistellig mm Minuten, zweistellig SS Sekunden, zweistellig 0 + oder abhängig davon, ob sich die Zeitzone, in der sich das Dokument befindet, östlich (-) oder westlich (+) des Nullmeridians liegt HHr Anzahl der Stunden, die die eigene Zeitzone gegenüber UTC verschoben ist. Der Wert ist zweistellig mit' dahinter anzugeben. mm’ Anzahl der Minuten, die die eigene Zeitzone im Vergleich zu UTC verscho- ben ist. Der Wert ist zweistellig mit nachfolgendem ' anzugeben. Tabelle 6.5 Platzhalter im Datumsformat Beispielsweise würde sich für den 25.02.2008, 12:15 Uhr in Deutschland die fol- gende Darstellung ergeben: 0:20080225121500+01'00' Da dieses Datum in der Winterzeit, also der Normalzeit, liegt, ist die Angabe des Offsets unproblematisch. Bei einer Sommerzeitangabe kann man sicher darüber diskutieren, ob Sie dort +01 oder +02 angeben müssen. Ich konnte leider nicht in 332
e-bol.net Generieren von PDF-Dokumenten | 6.3 Erfahrung bringen, welche Variante korrekt ist. In den meisten Zusammenhän- gen dürfte das aber auch nicht ins Gewicht fallen. 6.3.7 Einlesen von PDF-Dokumenten Zum Schluss möchte ich Ihnen noch die statische Methode 1 oad() vorstellen. Sie ist in der Klasse Zend_Pdf deklariert und ermöglicht es Ihnen, ein bereits beste- hendes PDF-Dokument zu laden. Sie liefert daraufhin ein Zend_Pdf-Objekt zurück, das Sie mit allen bereits beschriebenen Methoden verändern können. Leider gibt es keine Möglichkeit, die bereits vorhandenen Inhalte auszulesen. Aber auch das ist für zukünftige Versionen geplant. Trotzdem haben Sie damit aber eine gute Möglichkeit, mit Vorlagen wie zum Beispiel einem fertigen Brief- kopf zu arbeiten und diesen dann nur um die Anschrift und den Text zu ergän- zen. 333
e-bol.net
e-bol.net Bender: »Ich habe von lauter Nullen und Einsen geträumt. Und ich dachte, ich hätte irgendwo eine 2 gesehen!« Fry: »Ach Bender, sowas wie eine 2 gibt es doch gar nicht!« (aus der Serie Futurama) 7 Protokolle und Co. Es liegt in der Natur der Sache, dass Webanwendungen mit ihren Clients oder auch mit anderen Servern kommunizieren müssen. Auch dabei unterstützt Sie das Zend Framework. Neben der Möglichkeit, direkt via HTTP zu kommunizie- ren, können Sie auch Cookies lesen und schreiben oder Webservices mit XML- RPC oder REST anbieten oder nutzen. 7.1 Zugriff auf andere Server mit Zend_Http Mithilfe der Zend_Http-Klassen können Sie einen HTTP-Client mit allen relevan- ten Features erstellen. Üblicherweise wird ein Browser als HTTP-Client genutzt, sodass sich die Frage stellt, wofür diese Klassen denn gut sein mögen. Mithilfe von PHP einen HTTP-Client zu erstellen, ist beispielsweise sehr hilfreich, wenn Sie aus PHP heraus Daten an ein Formular übergeben wollen, um sich bei einem Internetdienst anzumelden oder Daten automatisiert hochladen möchten. Kurz gesagt, wann immer Sie sich wünschen würden, dass PHP die Aufgaben eines Browser wahrnehmen soll, sind die Zend_Http-Klassen sehr hilfreich. Das Paket gliedert sich im Wesentlichen in die Klassen Zend_Http_Cl ient, Zend_ Http_Cookie, Zend_Http_CookieJar und Zend_Client_Response. Eine Klasse Zend_Http existiert nicht. 7.1.1 Das HTTP-Protokoll Bevor ich auf die Nutzung der Klassen eingehe, möchte ich kurz das HTTP-Proto- koll betrachten. HTTP (Hypertext Transfer Protocol) ist dasjenige Protokoll, das für den Austausch von Daten im World Wide Web genutzt wird. Es handelt sich dabei um ein recht einfach aufgebautes Protokoll, das nach dem Pull-Prinzip funktioniert. Das heißt, der Client schickt eine Anfrage an den Server, welcher 335
e-bol.net 7 | Protokolle und Co. daraufhin antwortet. Die Antwort wird vom Client entgegengenommen und dann verarbeitet bzw. dargestellt. Dies ist bereits die grundsätzliche Idee hinter der Kommunikation. Bei dieser Kommunikation übermittelt der Client Daten an den Server. Diese Daten werden im Klartext versendet. Dabei teilt der Client dem Server mit, wel- che Information bzw. Webseite er anfordert, welche Sprachen (Deutsch, Englisch etc.) der Benutzer bevorzugt, um welche Art Client es sich handelt und Ähnli- ches. Bei der Antwort des Servers sind neben den eigentlichen Daten, wie zum Beispiel der HTML-Seite, auch Header enthalten, die noch zusätzliche Informati- onen enthalten. Nachdem Sie nun einen kleinen Überblick über das Protokoll erhalten haben, können wir loslegen. 7.1.2 Einen HTTP-Client erstellen Um eine Information bei einem Server anzufordern, müssen Sie eine Anfrage an den Server senden. Hierfür ist die Klasse Zend_Http_Cl ient vorgesehen. Diese schickt die Anfrage, den sogenannten Request, an den Server und nimmt die Ant- wort entgegen. Die Daten und Informationen, die in der Antwort enthalten waren, erhalten Sie in Form eines Zend_Http_Response-Objekts zurück. Das folgende Beispiel zeigt die grundsätzliche Funktionsweise: require_once ’Zend/Http/Client.php’; // Objekt ableiten Sclient = new Zend_Http_Cl1ent('http://www.heise.de'); // Anfrage an Server senden Sresponse = $client->request(): // Antwort ausgeben echo $response->getBody(): Listing 7.1 Auslesen von Daten via HTTP In diesem Listing wird ein Zend_Http_Cl ient-Objekt abgeleitet, dem die URL übergeben wird, die eingelesen werden soll. Bitte beachten Sie, dass es sich dabei immer um eine komplette URL inklusive des Protokolls handeln muss. Der Auf- ruf der Methode request() schickt die Anfrage an den Server und gibt die Ant- wort als Zend_Http_Response-Objekt zurück. Mit der Methode getBody() kann dann der Körper der Antwort, in diesem Fall also die Startseite von www.heise.de, ausgelesen werden. Die Ausgabe des Scripts sehen Sie in Abbildung 7.1. 336
e-bol.net Zugriff auf andere Server mit Zend_Http | 7-1 heise online O http://carsten-mohrkes-computei“<Q” Google heise heise heise online • c't • iX • Technology Review • Telepolis • mobil • Security • Netze • —— • • Autos • c’t-TV open rcsaic [jj] heise online 15.102007 Meldungen des Tages Suche... Interpol identifiziert weltweit gesuchten Sexualstraftäter |M|news "fews-Archiv ^ews unterwegs Mcws einbinden Das aus verfremdeten digitalen Fotos rekonstruierte und im Internet veröffentlichte Gesicht des Verdächtigen hatte zu zahlreichen Hinweisen geführt; eine Überwachungskamera filmte den Mann auf einem Flughafen in Thailand, mehr... TELEPOLIS Re-Reed ucation oder: Kunst und Konditionierung Vom Krieg um das Unterbewusste zur Diktatur des Digitalen rclefontanfc Motorola wird Miteigentümer der UIQ-Entwicklerschmiede [ntemetstömngen Software/Download [T-Markt Der US-amerikanische Handyhcrsteller übernimmt von Sony Ericsson 50 Prozent der Anteile am Hersteller der UIQ-Bedicnoberfläche für Symbian-Handys. Die Unternehmen wollen den Kreis der UIQ-Eigner noch erweitern, mehr... heise Security PHP - aber sicher! Die Grundsicherung des eigenen Webspace ist schnell erledigt und kann eine Menge Arger sparen. Transferring data from www.toughbook.eu... Vi Abbildung 7.1 Darstellung der ausgelesenen Daten im Browser Dass die Darstellung nicht perfekt ist und einige Grafiken fehlen, liegt daran, dass die Pfade im HTML-Code relativ angegeben wurden und die entsprechenden Dateien nicht auf dem Rechner zu finden sind, der die Anfrage gestellt hat. Soll die Kommunikation mit dem Server nicht auf dem Standard-Port erfolgen, so können Sie den gewünschten Port direkt an den Namen des Servers, genauer gesagt an die Toplevel-Domain bzw. die IP-Adresse anhängen: http://www.example.com:88/daten.php Der Client kann noch weitgehender konfiguriert werden. Neben der URL können Sie dem Konstruktor noch ein Array mit bestimmten Schlüsseln übergeben, die das Verhalten des Clients verändern. Erlaubt sind dabei die Schlüssel aus Tabelle 7.1. Schlüssel I Bedeutung maxredi rects Maximale Anzahl der Weiterleitungen. Default ist 5. stri ctredi rects Strikte Redirects nach RFC 2626 durchführen. Ist der Wert true, wer- den die Redirects strikt durchgeführt und die Request-Methode bleibt erhalten. Tabelle 7.1 Schlüssel zur Konfiguration des HTTP-Clients 337
e-bol.net 7 | Protokolle und Co. Schlüssel Bedeutung useragent User-Agent, den der Client an den Server übermittelt timeout Maximale Wartezeit bis zum Time-out in Sekunden. Der Default-Wert beträgt 10 Sekunden. httpversi on Die HTTP-Version, die genutzt werden soll; Version 1.1 oder 1.0; Default-Wert ist 1.1. keepali ve Keepalive-Verbindungen nutzen (true) oder nicht (false). Default- Wert ist fal se. Tabelle 7.1 Schlüssel zur Konfiguration des HTTP-Clients (Forts.) Der Schlüssel, den Sie wahrscheinlich am häufigsten brauchen werden, ist user- agent. Mit ihm setzen Sie den String, mit dem der Client dem Server mitteilt, um welche Art »Browser« es sich handelt. Einige Internetangebote liefern unter- schiedliche Inhalte in Abhängigkeit vom Browser aus oder sperren Inhalte sogar, falls es sich nicht um einen »normalen« Browser handelt, um automatisierte Downloads zu unterbinden. Möchten Sie die Werte nicht direkt an den Konstruktor übergeben, können Sie die URL auch über die Methode setUri () und das Konfigurations-Array mithilfe von setConfi g() an das Objekt übergeben. 7.1.3 Übergabe von Werten Das alles wäre natürlich nur halb so spannend, wenn Sie beim Aufruf einer URL keine Werte übergeben könnten. Sie haben die Möglichkeit, Daten mithilfe von GET und/oder POST an den Server zu übergeben und somit das Verhalten eines Formulars zu simulieren. In dem folgenden Beispiel werden Daten an ein Script übergeben, das normaler- weise Daten aus einem Formular übernehmen würde: Spage = $_GET[1page'1; Suser = $_POST['user']; Spasswd = $_POST['password'1; if ('Jake' == Suser && 'geheim' — Spasswd) { echo "Sie sind eingeloggt und wollten Spage sehen<br>"; echo "Ihr Browser ist ein $_SERVER[HTTP_USER_AGENT]";"; 1 el se ( 338
e-bol.net Zugriff auf andere Server mit Zend_Http | 7-1 echo "Username und/oder Passwort waren falsch"; } Der Code, um ein Formular zu simulieren und Daten an dieses Script zu überge- ben, könnte so aussehen: require_once ’Zend/Http/Client.php’; // Objekt ableiten Sclient = new Zend_Http_Client(); // URI setzen $client->setUri(’http://127.0.0.1/auswertung.php'); // Konfiguration setzen (Kennung eines Firefox unter OS X) $client->setConfig(array( ’useragent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; 'de; rv:1.8.1.7) Gecko/20070914 '. 'Fi refox/2.0.0.7')); // GET-Parameter setzen $client->setParameterGet(’page', 'index.html'); // POST-Werte setzen $cli ent->setParameterPost(array( ’user' => 'Jake’, ’password' => 'geheim' )): // Request abschicken Sresponse = $client->request(Zend_Http_Client::POST); // Ergebnis ausgeben echo "<b><u>Antwort des Servers war :</uX/b><br>"; echo $response->getßody(); Listing 7.2 Datenübergabe mithilfe von Zend_Http Die GET-Parameter werden mithilfe der Methode setParameterGet() festgelegt, was für die POST-Werte mit der Methode setParameterPost() geschieht. Beiden Methoden können Sie entweder ein Array übergeben, bei dem die Schlüssel als Variabiennamen fungieren, oder Sie können zwei Parameter übergeben, bei dem der erste der Name und der zweite der Wert ist, um nur einen Wert zu setzen. Bei der Nutzung dieser Methoden ist es nicht notwendig, die Werte vorher an uri encode() zu übergeben; das Paket codiert sie automatisch korrekt. Die Methode request() bekommt bei diesem Aufruf den Parameter Zend_Http_ Client:: POST übergeben. Dies ist notwendig, weil POST-Parameter an den Server übergeben werden sollen. Standardmäßig wird ein Request immer als GET- 339
e-bol.net 7 | Protokolle und Co. Anfrage verschickt. Wird der Parameter nicht übergeben, resultiert daraus eine Exception. Zwar werden Sie andere Request-Arten nicht so oft benötigen, aber Sie können auch die Konstanten PUT, HEAD, DELETE, TRAGE, OPTIONS oder CONNECT nutzen, um einen entsprechende HTTP-Request zu initiieren. 7.1.4 Uploads Oft kann es sehr hilfreich sein, nicht nur reine Textdaten, sondern auch Dateien an ein Script zu übergeben. Sind Sie beispielsweise stolzer Besitzer einer Digital- kamera und wollen Ihre Bilder im Internet präsentieren, könnte es sein, dass das Online-Photoalbum, das Sie nutzen wollen, keine FTP-Uploads unterstützt und ein paar hundert Bilder über ein HTML-Formular hochzuladen ist vielleicht ein wenig mühsam. Ein kleines Script wäre in dem Fall hilfreich. Auch das Übertragen von Dateien ist denkbar einfach. Für einen File-Upload ist die Methode setFileUpload() zuständig. Interessant an der Methode ist, dass sie entweder eine Datei einlesen oder auch direkt Daten übergeben bekommen kann, die als Datei übertragen werden. Das ist beispielsweise dann spannend, wenn Sie Daten aus einer Datenbank übernehmen und diese als CSV-Datei über- tragen wollen. Die Methode erhält als ersten Parameter den Namen der Datei übergeben. Die- sen müssen Sie auch dann übergeben, wenn die Datei nicht direkt von der Fest- platte gelesen, sondern als String übergeben wird. In diesem Fall dürfen Sie sich natürlich einen Namen ausdenken. Der zweite Parameter ist derjenige Name, den das Input-Feld innerhalb des Formulars hätte (wenn es sich denn um ein For- mular handeln würde). Bei einem gewöhnlichen Datei-Upload benötigen Sie keine weiteren Parameter. Wollen Sie Daten direkt übergeben, so übergeben Sie diese als dritten Parameter. Der vierte Parameter, der auch optional ist, gibt Ihnen die Möglichkeit, den MIME-Type zu übergeben. Übergeben Sie ihn nicht, ermittelt die Methode selbst einen MIME-Type. Bei einer Datei wird versucht, mithilfe der Funktion mime_content_type() den korrekten MIME-Type zu ermitteln. Ist das nicht möglich oder haben Sie die Daten als Parameter überge- ben, nutzt die Methode appl ication/octet-stream als MIME-Type. require_once 'Zend/Http/Client.php'; Sclient = new Zend_Http_Client('http://127.0.0.1/upload.php'); // Textdaten direkt übertragen // Textdaten aufbereiten (Normalerweise Daten aus Datenbank) $csv_daten = "Monat;Umsatz 340
e-bol.net Zugriff auf andere Server mit Zend_Http | 7-1 Januar:1212.00 Februar;3443.12"; // Upload vorbereiten $client->setFi1eUpload('umsaetze.csv', // Dateiname ’umsatzdatei', // Feldname im Formular $csv_daten, // Daten ’text/csv'); // MIME-Type // Datei von Platte lesen und übertragen $client->setFi1eUpload('urlaub.jpg' , 'bild'); // Datei von Platte lesen und MIME-Type manuell setzen $client->setFi1eUpload('einkauf.doc', 'einkaufsliste' , null, 'application/msword'); // Dateien als POST-Request übertragen $cl ient->request(Zend_Http_Client::POST); Listing 7.3 Upload von Dateien Sie müssen sich nicht darum kümmern, dass der Encoding-Typ korrekt gesetzt wird; das übernimmt die Klasse für Sie. Bei der Arbeit mit Dateien sollten Sie immer auf ein korrektes Exception Handling achten, da die Klasse eine Exception wirft, falls die Datei nicht gelesen werden kann. 7.1.5 HTTP-Authentifikation In vielen Fällen ist es sinnvoll, Formulare oder andere Ressourcen mit einem Passwort zu sichern. Hierbei sind zwei Fälle zu unterscheiden: Haben Sie die Authentifikation auf Basis eines Formulars selbst implementiert, ist der Sachver- halt unproblematisch. Sie können Benutzernamen und Passwort einfach als nor- male Formularwerte übergeben. Sollten Sie aber eine HTTP-Authentifikation, also beispielsweise eine .htaccess- Datei nutzen, sieht die Sache anders aus. In diesem Fall müssen Sie den Benutzer- namen und das Passwort an die Methode setAuth() übergeben: $cl1ent->setAuth('user', 'geheim'); Standardmäßig führt der Client eine Basic-Authentifikation aus. Die Digest- Methode ist noch nicht implementiert. Sollte das in Zukunft der Fall sein, so kön- nen Sie an dritter Stelle die Konstante Zend_Http_Cl i ent: :AUTH_DIGEST überge- ben, um eine Digest-Authentifikation durchzuführen. 341
e-bol.net 7 | Protokolle und Co. 7.1.6 Server-Antworten auswerten In den vorangegangenen Beispielen wurde immer nur direkt der Körper der Ant- wort ausgegeben, der mit getBody() ausgelesen wurde. Diese Vorgehensweise ist für einen produktiven Einsatz nicht unbedingt geeignet. Immerhin könnte es sein, dass die gewünschte Seite auf dem Server nicht gefunden werden kann. In diesem Fall liefert der Server zwar auch ein Dokument zurück, aber diese Fehler- seite möchte man wahrscheinlich nicht direkt ausgeben. Dazu ist es angebracht, die Header zu prüfen. Hierin ist nämlich auch ein Statuscode enthalten, dem Sie entnehmen können, ob die Operation erfolgreich war. Den Statuscode können Sie mit der Methode getCode() und die dazugehörige Statusmeldung mit getMessage() auslesen: require_once ’Zend/Http/Client.php’; Sclient = new Zend_Http_Client('http://www.netviser.de'); Sresponse = $client->request(): echo "URL: ".$client->getUri()."<br>"; echo ''Statuscode: ’’. $response->getStatus(). "<br>"; echo "Statusmeldung: ".$response->getMessage(). "<br>"; $client->setUri(’http://www.netvi ser.de/gibtsnicht.html’); $response = $client->request(); echo "<br>URL: ".$client->getUri()."<br>"; echo "Statuscode: ".$response->getStatus()."<br>"; echo "Statusmeldung: ".$response->getMessage().”<br>"; Listing 7.4 Auswerten einer Server-Antwort Die Ausgabe dieses Listings sehen Sie in Abbildung 7.2. Abbildung 7.2 Auswerten von Server-Antworten 342
e-bol.net Zugriff auf andere Server mit Zend_Http | 7-1 Der Statuscode und die Statusmeldung, die das System zurückgeben, entsprechen dem, was der HTTP-Standard definiert. Bitte beachten Sie, dass diese Codes nur dann ausgewertet werden können, wenn der angegebene Server antwortet. Exis- tiert der Server nicht, erhalten Sie kein Antwortobjekt, sondern es wird eine Exception geworfen. Den Code manuell auszuwerten, wäre meist zu aufwändig. Daher sind in der Klasse einige Funktionen definiert, mit denen Sie jeweils mehrere Fälle auf ein- mal überprüfen können. Das ist recht einfach, da die Codes jeweils in Blöcken zusammengefasst sind. Die 2XX-Codes teilen dem Client eine erfolgreiche Opera- tion mit, die 3XX-Codes stehen dafür, dass eine Weiterleitung erfolgte, die 4XX- Codes teilen Ihnen mit, dass ein Client-Fehler aufgetreten ist, und die Codes, die mit 5 beginnen, sagen aus, dass ein serverseitiger Fehler aufgetreten ist. Um dies möglichst einfach zu prüfen, sind die Methoden isSuccessful1(), i sRedi rect() und i sError() definiert, die feststellen, ob der Statuscode mit 2, 3 bzw. 4 oder 5 beginnt. Zwischen Client- und Server-Fehlern wird hierbei nicht unterschieden. Viele Anbieter im Internet nutzen gerne die 3XX-Statuscodes, wenn sich Pfade beispielsweise durch einen Relaunch geändert haben. Dadurch kann eine alte URL aufgerufen werden, und der Server kann dem Client mitteilen, dass die Seite jetzt unter einer anderen URL zu finden ist. Rufen Sie eine solche URL mit Zend_ Http auf und akzeptieren Sie Redirects, so erhalten Sie zum Schluss nur den Sta- tuscode 200, der Ihnen mitteilt, dass die Seite gefunden wurde. Allerdings wäre es sinnvoll, die neue URL in Ihrem Datenbestand zu aktualisieren, damit Ihr Cli- ent nicht immer den Redirects folgen muss. Möchten Sie den gesamten Weg des Clients mitverfolgen, können Sie das mit den genannten Methoden schnell umsetzen: require_once 'Zend/Http/Client.php'; // URL bei der wir starten Sstart = ’http://12Z.0.0.1/redirect.php’ Sclient = new Zend_Http_Client($start): // Redirects nicht folgen $cl ient->setConfig(array(’maxredirects' => 0)): do { // Request ausführen $response = $client->request(): // War es eine Weiterleitung? ifttrue ==— $response->isRedirect()) 343
e-bol.net 7 | Protokolle und Co. { // Dann speichern wir den Code und die Ziel-URL $urls[]= array('url'=>(string)$client->getUri(), ’code’=>$response->getStatus()); // Die Schleife soll jedem Redirect folgen } while (true —== $response->isRedirect()); // Ist ein Fehler aufgetreten? if (true === $response->isError()) { echo "Fehler: ".$response->getStatus(): exlt: // Sind wir bei einem 2XX-Code angekommen? if (true — $response->isSuccessful()) { echo "Start: $start<br>"; foreach ($urls as $url) { echo "Code $url[code] => $url[url]<br>"; echo "Ende: ".$client->getUri().”<br>"; echo "Statuscode: ".$response->getStatus()."<br>"; echo "Statusmeldung: ".$response->getMessage()."<br>"; I Listing 7.5 Umgang mit Redirects Interessant an diesem kleinen Script ist, dass das Ziel des Redirects, also die URL, die der Server zurückliefert, direkt im Client-Objekt abgelegt wird. Sie müssen die URL also nicht auslesen. Die URL, die ursprünglich im Client-Objekt enthal- ten war, ist überschrieben worden. Somit können Sie bei einem HTTP-Code 301, also einem »Moved Permanently« die URL in ihrem Datenbestand aktualisieren. Die Ausgabe dieses Scripts könnte nach einigen Weiterleitungen beispielsweise so aussehen: Start: http://127.0.0.1:80/ziel.php Code 301 => http://127.0.0.1:80/redirect2.php Code 301 => http://127.0.0.1:80/redirect3.php Code 307 => http://127.0.0.1:80/ziel.php Ende: http://127.0.0.1:80/ziel.php 344
e-bol.net Zugriff auf andere Server mit Zend_Http | "JA Statuscode: 200 Statusmeldung: OK Für die meisten Anwendungen werden diese Methoden ausreichen. Allerdings kennt die Klasse noch mehr Möglichkeiten. Möchten Sie zum Beispiel wissen, mit was für einer Art Server Sie es zu tun haben, können Sie mit der Methode getHeaderO die Kennung des Servers auslesen. Dazu übergeben Sie der Methode den Namen des Headers, dessen Wert Sie auslesen wollen - in diesem Fall also Server: echo $response->getHeader('Server'); // mögliche Ausgabe: Apache/1.3.33 (Darwin) PHP/5.2.4 Um alle Header auf einmal auszulesen, rufen Sie getHeadersO auf, womit Sie alle Header in Form eines Arrays zurückerhalten. Die Namen der Header fungie- ren hierbei als Schlüssel. Bitte beachten Sie in beiden Fällen, dass ein Header mehrfach enthalten sein kann. In dem Fall liefern beide Methoden ein Array für den Header zurück. Benötigen Sie die Header als String zur direkten Ausgabe, hilft Ihnen getHeadersAsStri ng() weiter. In den bisherigen Beispielen bin ich immer davon ausgegangen, dass der Server HTML-Code zurückgibt, der anschließend direkt ausgegeben werden kann. Nun könnte es aber auch passieren, dass Sie mit einer Weiterleitung auf eine PDF- Datei verwiesen werden. Um solche Situationen behandeln zu können, müssen Sie natürlich erkennen, dass es sich nicht um HTML handelt. Um zu erkennen, welche Art von Inhalt der Client erhalten hat, empfiehlt es sich, den MIME-Type zu nutzen, den der Server mitgeschickt hat. Mit den folgenden Zeilen können Sie den aktuellen Katalog von Galileo Press als PDF-Dokument herunterladen und speichern: require_once 'Zend/Http/Client.php'; $url = 'http://www.galileocomputing.de/download/'. 'katalog/gali1eo_press_katalog_computing.pdf’: Sclient = new Zend_Http_Client($url); Sresponse = $client->request(): if (true — $response->isSuccessful()) { // MIME-Type auslesen $mime = $response->getHeader('content-type'); // Sicherheitshalber auf Array prüfen if (true === is_array($mime)) { $mime = $mime[0]: 345
e-bol.net 7 | Protokolle und Co. I // HTML oder Text? if ('text/html' == $mime && ’text/plain' == $mime) { // Dann direkt ausgeben echo $response->getßody(); el se { // Kein Text also Daten in Datei speichern Scontent =$response->getßody(); Sfilename = basename($cl ient->getllri ()); file_put_contents($filename, Scontent); echo "Sfilename erfolgreich heruntergeladen"; } Listing 7.6 Herunterladen einer Datei Die Response-Klasse nutzt intern eine ganze Reihe sehr hilfreicher Funktionen, um Antworten von Servern zu analysieren. Diese sind alle statisch definiert und können von außen aufgerufen werden, sodass Sie diese auch dann nutzen kön- nen, wenn Sie selbst einmal Server-Antworten analysieren wollen. 7.1.7 Cookies Um einen komplexeren HTTP-Client zu erstellen, werden Sie eine Möglichkeit benötigen, Cookies zu verwalten. Viele Webanwendungen identifizieren die Cli- ents mithilfe eines Cookies, indem eine Session-ID auf diesem Weg gespeichert wird. Darüber hinaus können auf diesem Weg auch andere Daten beim Client abgelegt werden, die später wieder eingelesen werden. Beliebt ist diese Vorge- hensweise auch bei Kundennummern und Ähnlichem, wobei sich hier stets die Frage nach der Sicherheit der Daten stellt. Maximal können in einem Cookie 4096 Bytes an Daten abgelegt werden. Zend_Http unterstützt ein recht ausgefeiltes Cookie-Management. So können Sie Cookies vom Server entgegennehmen, speichern und dann beim nächsten Request wieder mit übertragen. Darüber hinaus können Sie die Cookies auch analysieren und manipulieren und sogar neue Cookies für eine Domain anlegen. Cookies auslesen Die Cookies werden vom Client in einem Objekt der Klasse Zend_Http_Cookie- Jar verwaltet. Soll Ihr Client Cookies verwalten, müssen Sie zunächst ein 346
e-bol.net Zugriff auf andere Server mit Zend_Http | 7-1 CookieJar anlegen, indem Sie die Methode setCookieJari) aufrufen. Der Client legt darin dann die Cookies ab. Dieses Objekt können Sie nach dem Ausfuhren des Requests mit der Methode getCookieJar() auslesen. Das Auslesen des Objekts ist ein wichtiges Feature, da das Client-Objekt die Cookies natürlich nicht speichern kann. Möchten Sie die Cookies später nutzen, damit der Server Sie wieder identifizieren kann, so müssen Sie das Cooki eJar-Objekt in einer Session, Datei oder Datenbank Zwischenspeichern. Bitte vergessen Sie nicht, das Objekt vor dem Speichern zu serialisieren. Im folgenden Beispiel wurde dieses Script genutzt, um Cookies zu setzen. Es han- delt sich also so zu sagen um den Server der mithilfe von Zend_Http_Client angesprochen werden soll. < ? php setcookie('Frage’, 'nach dein Leben, dem Universum und dem ganzen Rest'); setcookie('Antwort',42); ?> Listing 7.7 Setzen von Cookies Alternativ können Sie für Ihre Tests auch jede beliebige Homepage nutzen, die Cookies nutzt, wie zum Beispiel www.amazon.de. Damit Ihr HTTP-Client Cookies entgegennehmen kann, benötigen Sie, wie schon erwähnt, einen CookieJar. Um diesen zu initialisieren, rufen Sie die Methode setCookieJar() auf. Dieser können Sie ein CookieJar-Objekt oder true überge- ben, was dem Default-Wert entspricht. Übergeben Sie nichts oder true, wird ein neues Objekt abgeleitet und genutzt. Sobald Sie dann den Request an den Server senden - der CookieJar muss also vorher initialisiert werden - werden die Cookies als einzelne Objekte im Jar abgelegt. Um die Cookies auszulesen, steht die Methode getCookiesi) zur Verfügung, welche Ihnen ein Cooki eJar-Objekt zurückgibt. Anders als viele andere Klassen implementiert die dazugehörige Klasse leider keinen Iterator, sodass Sie die Cookies zunächst auslesen müssen. Das kann beispielsweise mit der Methode getAllCookies() geschehen, die Ihnen die Cookies als Array zurückgibt: < ? php require_once 'Zend/Http/Client.php'; $url = ’http://127.0.0.1/set_cookie.php’; Sclient = new Zend_Http_Client($url); $client->setCookieJar(); $client->request(); 347
e-bol.net 7 | Protokolle und Co. $cookie_jar = $cl1ent->getCookieJar(); $cookies = $cookie_jar->getAHCookies(); foreach ($cookies as tcookie) { echo "<p>"; echo "Domain: ".$cookie->getDomain(); echo "<br>"; echo "Name: ".$cook1e->getName(): echo "<br>"; echo "Wert: ".$cook1e->getValue(); echo "</p>"; I ?> Listing 7.8 Auslesen von Cookie-Daten Das Array, das getAl 1 Cookies () zurückliefert, enthält Objekte der Klasse Zend_ Http_Cooki e. Die Informationen, die in den Cookies enthalten sind, können Sie über bestimmte Methoden auslesen. In diesem Beispiel wurden getDomaint), getName() und getValue() genutzt, um die Werte der entsprechenden Eigen- schaften auszulesen. Bei der Domain handelt es sich um die Domain bzw. die IP- Adresse des Servers; Name und Wert sind selbsterklärend. Die Ausgabe des Scripts sehen Sie in Abbildung 7.3. O http://127.0.0.l/~cars.. buch/http_Client/6.php CD w .«>© O http://127.0-0.1/-CJ'Q’ Google Domain: 127.0.0.1 Name: Frage Wert nach dem Leben, dem Universum und dem ganzen Rest Domain: 127.0.0.1 Name: Antwort Wert: 42 Abbildung 7.3 Ausgelesene Cookie-Daten In der Cookie-Klasse sind natürlich noch weitere Methoden deklariert, mit denen Sie zusätzliche Informationen auslesen können. Mit getExpiryTimet) können Sie den Timestamp auslesen, der festlegt, wie lange ein Cookie gültig ist. get- Path() ist diejenige Methode, die Ihnen den Pfad der Datei liefert, welche das Cookie gesetzt hat. Die Methoden isExpiredO, isSecureO und isSession() liefern jeweils einen booleschen Wert zurück. Die erste prüft, ob ein Cookie ver- fallen ist. Übergeben Sie keinen Timestamp, so geht die Methode von der aktuel- 348
e-bol.net Zugriff auf andere Server mit Zend_Http | "JA len Serverzeit aus. Sollte ein Cookie keine Verfallszeit haben, liefert die Methode stets false zurück. Ein Session-Cookie hat übrigens nichts mit einer Session in PHP zu tun. »Session-Cookie« bedeutet lediglich, dass das Cookie nur während der Session, also so lange, wie der Browser geöffnet ist, gültig ist. Ein solches Ses- sion-Cookie können Sie übrigens auch jederzeit daran erkennen, dass die Methode i sSession() den Wert true zurückgibt. Die Methode i sSecuref) teilt Ihnen mit einem booleschen Wert mit, ob es sich um ein sicheres Cookie han- delt, das nur über verschlüsselte Verbindungen ausgetauscht werden sollte. Die Methode getAl 1 Cooki es () kann Ihnen die Daten eines Cookies auch direkt als String zurückgeben, falls Ihnen das lieber sein sollte. In diesem Fall können Sie der Methode beim Aufruf entweder Zend_Http_Cooki eJar:: COOKI E_ST RIN G_ ARRAY oder Zend_Http_CookieJar::COOKIE_STRING_CONCAT übergeben. Im ers- ten Fall wird jedes Cookie als einzelner String im Array abgelegt, sodass die bei- den folgenden Strings enthalten sind: Frage=nach+dem+Leben%2C+dein+Uni versum+und+dem+ganzen+Rest; Antwort=42; Die Daten sind also URL-codiert, was Sie ändern können, indem Sie die Daten mit der Funktion urldecodel) decodieren. Mit der zweiten Konstanten sind die Daten alle in einem String abgelegt, wobei nach jedem Cookie ein Semikolon folgt. Bei den bisherigen Vorgehensweisen wurden immer alle Cookies ausgelesen. Sie können aber auch einzelne Cookies ansprechen, sofern das für Sie hilfreich sein sollte. Dazu greifen Sie auf getMatchi ngCookiesf) oder getCookie() zurück. Die erste Methode bekommt als ersten Parameter die URI übergeben, unter der die Cookies gesetzt wurden. Die URI setzt sich zusammen aus der Domain bzw. IP-Adresse des Servers und dem Pfad der Datei, welche die Cookies gesetzt hat. Sie liefert dann ein Array mit allen Cookie-Objekten zurück, die unter dieser URI gesetzt wurden. Als zweiten Parameter können Sie einen booleschen Wert über- geben, der der Methode mitteilt, ob auch Session-Cookies mit zurückgegeben werden sollen. Dies ist per Default der Fall. Als dritten Parameter können Sie auch hier die Konstanten Zend_Http_CookieJar: :COOKIE_STRING_ARRAY und Zend_Http_CookieJar: :COOKIE_STRING_CONCAT nutzen, um die Rückgabewerte zu beeinflussen. Die Methode getCookiel) ist ein wenig präziser und bekommt als ersten Para- meter ebenfalls die URI übergeben. Der zweite Parameter ist der Name des Cookies, das ausgelesen werden soll. 349
e-bol.net 7 | Protokolle und Co. Um das Cookie Frage aus dem obigen Beispiel auszulesen, können Sie also die folgende Zeile nutzen, die Ihnen das Cookie als Objekt zurückgibt: Scookie = $cookie_jar->getCookie(’http://127.0.0.1/’,'Frage'): Was allerdings voraussetzen würde, dass die Datei, die das Cookie gesetzt hat, im Root-Verzeichnis des Servers liegt. Würde sie beispielsweise im Ordner cookies liegen, so müsste die Methode wie folgt aufgerufen werden: Scookie = $cookie_jar->getCookie(’http://127.0.0.1/cookies/’, 'Frage’); Als letzten Parameter können Sie noch eine der beiden String-Konstanten über- geben, die schon bei getAl 1 Cooki es() erwähnt wurden. In diesem Fall sorgen aber beide für das gleiche Ergebnis und geben einen String zurück. Cookies setzen Natürlich können Sie Cookies auch setzen. Sie können dazu entweder Cookie- Objekte nutzen, die Sie bereits vom Server erhalten haben, oder Sie leiten neue Objekte ab. Ein Manipulieren der Inhalte, die in einem empfangenen Cookie ent- halten sind, ist mit den momentan vorhandenen Methoden nicht möglich. Um ein neues Cookie-Objekt abzuleiten, müssen Sie dem Konstruktor mindes- tens den Namen, den Wert und die Domain bzw. die IP des Servers übergeben. Optional können Sie noch das Verfallsdatum als Timestamp, den Pfad und einen booleschen Wert übergeben, der festlegt, ob es sich um ein Cookie handelt, das nur über eine sichere Verbindung übertragen werden darf. Ein so konstruiertes Cookie-Objekt können Sie mit der Methode addCooki e() an das CookieJar-Objekt übergeben, welches Sie dann wiederum an das HTTP- Objekt übergeben müssen: < ? php require_once 'Zend/Http/Client.php’; require_once ’Zend/Http/Cookie.php’; requi re_once 'Zend/Http/CookieJar .php'; $url = ’http://127.0.0.1/get_cookie.php’; Sclient = new Zend_Http_Client($url); Scookie = new Zend_Http_Cookie('Vorname’, // Name des Cookies 'Friederike', // Wert des Cookies 'example.com', // Domain time()+3600): // Gültigkeit: Jetzt+lh 350
e-bol.net Zugriff auf andere Server mit Zend_Http | 7-1 $cookie_jar = new Zend_Http_CookieJar(); $cookie_jar->addCookie(Scookie); $cl 1ent->setCookieJar($cookie_jar); $client->request(); ?> Listing 7.9 Setzen von Cookies Das Script, das serverseitig aufgerufen wird, kann die Daten anschließend über das superglobale Array $_COOKIE auslesen. In diesem Beispiel könnten Sie den Wert des Cookies also über $_COOKI E[' Vorname' ] auslesen. Alternativ können Sie ein Cookie-Objekt auch ableiten, indem Sie der statischen Methode fromString() ebenfalls einen String übergeben, wie er mit dem Set- Cooki e-Header genutzt wird, um ein Cookie zu setzen: Scookie = Zend_Http_Cookie::fromString(’Vorname=Friederike: expires=Wednesday, 31-0ct-2007 23:59:59 GMT; domain=.example.com: path=/cookies; secure'): In diesem Beispiel würde ein Cookie mit dem Namen Vorname erstellt, das den Wert Friederike hat und am 31. Oktober 2007 um 23:59:59 Uhr verfällt. Des Weiteren gehört es zur Domain example.com, wurde unter dem Pfad cookies gesetzt und soll nur über sichere Verbindungen übertragen werden. Würden Sie das Schlüsselwort secure entfallen lassen, könnte das Cookie über jede Art von Verbindung übertragen werden. Ohne Datumsangabe würde es sich um ein Ses- sion-Cookie handeln. 7.1.8 Nutzung von Adaptern Bisher basieren alle Beispiele darauf, dass eine direkte Verbindung zum Internet besteht. In der Realität ist das natürlich nicht immer so. Nutzen Sie beispielsweise einen Intranet-Server, kann es schnell passieren, dass dieser über einen Proxy- Server mit dem Internet verbunden ist. Aber auch dafür ist Zend_Http gerüstet. Zend_Http unterstützt verschiedene Adapter, die unterschiedliche Zugriffsmög- lichkeiten ermöglichen. Standardmäßig nutzt das Paket den Socket-Adapter, der davon ausgeht, dass eine direkte Verbindung zum Internet besteht. Darüber hin- aus sind auch ein Proxy- und ein Test-Adapter vorhanden. Den Proxy-Adapter finden Sie nachfolgend erläutert. Der Test-Adapter ermöglicht es Ihnen, Ihre Applikationen zu testen, indem er zuvor definierte Antworten bereitstellt. Das heißt, Sie können testen, ohne direkt via HTTP auf den entsprechenden Dienstan- bieter zuzugreifen. Informationen zur Nutzung finden Sie im Manual. 351
e-bol.net 7 | Protokolle und Co. Konfiguration des Socket-Adapters In den meisten Fällen werden Sie den Socket-Adapter nicht weiter konfigurieren müssen. Allerdings haben Sie die Möglichkeit, noch kleinere Einstellungen vor- zunehmen. Dazu können Sie dem Konstruktor noch einen zweiten Parameter übergeben. Dabei handelt es sich um ein Array, das die Schlüssel aus Tabelle 7.2 enthalten darf. Schlüssel Erläuterung ssltransport Definiert, welcher Transport-Layer genutzt werden soll, ssl, sslv2 oder tl s sind mögliche Werte, wobei ssl der Standard ist. sslcert Kann den Pfad zu einem PEM-codierten SSL-Zertifikat enthalten. sslpassphrase Dient dazu, eine Passphrase für ein SSL-Zertifikat zu übergeben. Tabelle 7.2 Konfiguration des Socket-Adapters Verbindungsaufbau über einen Proxy Möchten Sie eine Verbindung über einen Proxy aufbauen, so können Sie auf den Proxy-Adapter zurückgreifen. In dem Fall übergeben Sie als zweiten Parameter ein Array, das die oben genannten Werte enthalten darf. Zusätzlich werden aber noch die Informationen benötigt, die den Zugriff auf den Proxy ermöglichen. Hierbei handelt es sich um die Schlüssel aus Tabelle 7.3. Schlüssel Bedeutung adapter Muss den Wert Zend_Http_Cl ient_Adapter_Proxy zugewiesen bekommen. proxy_host Name oder IP-Adresse des Proxy-Servers proxy_port Port, auf dem der Proxy angesprochen werden soll (Default: 8080) proxy_user Benutzername für den Proxy, sofern erforderlich proxy_pass Passwort für den Proxy, sofern erforderlich proxy_auth Authentifikationsverfahren für die Anmeldung beim Proxy-Server, Standard ist Zend_Http_Cl i ent:: AUTH_BASIC. Eine Digest-Authentifikation ist noch nicht implementiert. Tabelle 7.3 Konfiguration des Proxy-Adapters 7.2 URIs mit Zend_Uri verarbeiten Die Klasse Zend_Uri wird intern in vielen Klassen des Zend Frameworks genutzt. Sie dient dazu, URIs zu generieren und zu validieren. Zurzeit werden nur URIs mit HTTP- und HTTPS-Schema unterstützt. Im Quelltext findet sich ein Hinweis, dass 352
e-bol.net URIs mit Zend_Uri verarbeiten | 7-2 demnächst auch mailto-URIs unterstützt werden sollen. Aber das ist zurzeit noch nicht der Fall. Es kann also nicht schaden, wenn Sie einen Blick in die Dokumen- tation werfen, um zu erfahren, welche Möglichkeiten die Klasse inzwischen bietet. 7.2.1 URIs analysieren In den meisten Fällen wird es sicher so sein, dass Sie eine URI analysieren wollen, beispielsweise weil Sie über ein Formular eingegeben wurde. Die Gültigkeit einer URI können Sie mit der statischen Methode check() prüfen, welche einen booleschen Wert zurückliefert. requi re_once('Zend/Uri.php'); $uri = ’http://www.adminblogger.de/blog/'; if ( true === Zend_Uri::check($uri)) { echo 1 URI ist gültig': } el se { echo 'URI ist NICHT gültig': } Listing 7.10 Validieren einer URI In vielen Fällen wird diese Methode sicher schon eine große Hilfe sein, wobei Sie natürlich auch auf Zend_Val idate_Hostname zurückgreifen könnten. Zend_Uri kann aber noch mehr. Sie können beispielsweise einzelne Teile einer URI extra- hieren oder auch Strings, die in einer URI genutzt werden sollen, auf ihre Gültig- keit hin prüfen. Um die Einzelteile zu extrahieren, benötigen Sie zunächst ein entsprechendes Objekt. Sie könnten also direkt ein Zend_Uri_Http-Objekt ableiten oder die Fac- tory-Methode nutzen, die in der Klasse Zend_Uri deklariert ist: requi re_once('Zend/Uri.php'); Sstring = 'http://paule:geheim@www.example.com:88': Sstring .= '/daten/index.php?id=12&show=true#unten'; $uri = Zend_Uri::factory($string); echo "Protokoll: ".$uri->getScheme(); echo "<br>"; echo "User: ".$uri->getUsername(); echo "<br>"; 353
e-bol.net 7 | Protokolle und Co. echo "Passwort: ".$uri->getPassword(): echo *<br>": echo "Host: ".$uri->getHost(): echo "<br>"; echo "Port: ".$uri->getPort(): echo "<br>"; echo "Pfad: ".$uri->getPath(); echo "<br>"; echo "Query-String: ".$uri->getQuery(); echo "<br>”; echo "Anker: ".$uri->getFragment(): Listing 7.11 Bestandteile einer URI auslesen Die Ausgabe des Scripts sehen Sie in Abbildung 7.4. http://ca. .URI/l.php CD QQOU Protokoll: http User paule Passwort: geheim Host: www.examplc.com Port: 88 Pfad: /datcn/indcx.php Query-String: id=12&show=true Anker unten Abbildung 7.4 Bestandteile einer URI Sollten Sie also URIs analysieren müssen, so ist das sicher ein sehr einfacher und eleganter Weg. Die einzelnen Methoden sind selbsterklärend, sodass ich nicht weiter darauf eingehe. Ist einer der Werte nicht in der URI enthalten, so liefert die Methode false zurück. Mit der Methode val id(), die hier nicht genutzt ist, können Sie die Gültigkeit der übergebenen URI prüfen. Die komplette URI kön- nen Sie mit der Methode getUri () auslesen. Im obigen Beispiel ist dies nicht sinnvoll; aber Sie können eine URI mit dieser Klasse auch neu erstellen bzw. eine bestehende URI manipulieren. Dazu sind für jeden Teil der URI Methoden dekla- riert, deren Namen mit set beginnen, also zum Beispiel setHostO oder set- UsernameO. Das Schema, sprich das Protokoll (also HTTP), können Sie nicht ersetzen. Möchten Sie, bevor Sie die Werte in die URI übernehmen, erst testen, ob sie gültig sind, so können Sie zu diesem Zweck auf einen Satz von Methoden zugreifen, deren Name jeweils mit validate beginnt, zum Beispiel validate- Passwordt). Diese Methoden liefern jeweils einen booleschen Wert zurück. 354
e-bol.net Nutzung von XML-RPC mit Zend_XmlRpc | 7-3 requi re_once('Zend/Uri.php'): Suser = 'Barney' ; Spasswort = 'geheim'; Sport = 80: Sstring = 'http://www.example.com:88/index.php'; Suri = Zend_Uri::factory(Sstring): if (true === Suri->validateUsername(Suser)) { Suri->setUsername(Suser); } if (true === Suri->validatePassword(Spasswort)) { Suri->setPassword(Spasswort); } if (true — Suri->validatePort(Sport)) { Suri->setPort(Sport); } echo Suri->getUri(); // Ausgabe: http://Barney:geheim@www.example.com:80/index.php Listing 7.12 Erstellen einer URI 7.3 Nutzung von XAAL-RPC mit Zend_XmlRpc XML-RPC ist einer von vielen Standards, mit denen Sie Funktionalitäten auf anderen Servern ansprechen und ausführen können. Die Abkürzung XML-RPC steht für »Extensible Markup Language Remote Procedure Call«. Das System basiert darauf, dass XML-Daten über das HTTP-Protokoll ausgetauscht werden. Das heißt, es ist möglich, eine Prozedur auf einem entfernten Server anzuspre- chen und ihr Daten zu übergeben. 7.3.1 Allgemeines zu Zend_XmlRpc Beim Aufruf einer Prozedur via XML-RPC kommt es häufiger vor, dass die Funk- tionalität bestimmte Informationen benötigt, um ihre Aufgabe verrichten zu kön- nen. Diese Daten werden innerhalb des Requests mit zum Server übertragen. Auch wenn die Daten innerhalb von XML ja nur als einfache Textdaten darge- stellt werden, so übermittelt XML-RPC dennoch Informationen zu dem XML- 355
e-bol.net 7 | Protokolle und Co. RPC-Datentyp, welcher der Information zugrunde liegt. Hierbei ist zu beachten, dass Sie auch immer die korrekten Datentypen übergeben müssen. Standardmä- ßig können Sie davon ausgehen, dass die PHP-eigenen Datentypen direkt in die entsprechenden XML-RPC-Datentypen konvertiert werden können, wie Sie in Tabelle 7.4 sehen. PHP-Datentyp XML-RPC-Datentyp Klasse im Zend Framework Integer Integer Zend_Xml Rpc_Value_Integer Double Double Zend_XmlRpc_Value_Double Boolean Bolean Zend_XmlRpc_Value_Boolean String String Zend_XmlRpc_Value_Stri ng Array Array Zend_XmlRpc_Value_Array assoziatives Array Struct Zend_XmlRpc_Value_Struct Object Array Zend_XmlRpc_Value_Array - Base64 Zend_XmlRpc_Value_Base64 - DateTime.iso8601 Zend_XmlRpc_Value_DateTime Tabelle 7.4 XML-RPC-Datentypen Übergeben Sie eine PHP-Variable an eine der Zend_Xml Rpc-Methoden, so sollte der Wert in den korrekten Datentyp konvertiert werden. Alternativ können Sie aber auch immer ein Objekt der entsprechenden Klasse ableiten. 7.3.2 Erstellen eines XML-RPC-Servers Zend_Xml Rpc stellt komfortable Möglichkeiten zur Verfügung, einen XML-RPC- Server zu erstellen. Die grundsätzliche Idee ist einfach. Sie leiten ein Server- Objekt ab und teilen ihm mit, welche Funktionen bzw. Klassen ansprechbar sein sollen. Sobald Sie diese beim Server angemeldet haben, kann er die Anfrage ver- arbeiten. Ein ganz einfacher Server könnte so aussehen: requi re_once 'Zend/XmlRpc/Server.php'; // Funktion die im Server zur Verfügung stehen soll function hallo_sagen() { return "Hallo Welt": } // Server-Objekt ableiten Sserver = new Zend_XmlRpc_Server(): 356
e-bol.net Nutzung von XAAL-RPC mit Zend_XmlRpc | 7-3 // Funktion anmelden $server->addFunction('hal1o_sagen'); // Anfrage verarbeiten echo $server->handle(); Listing 7.13 Ein einfacher XML-RPC-Server Dieser Server kennt also nur die Funktion hal l o_sagen, die von außen angespro- chen werden kann. Auch wenn diese Funktion nur einen Wert zurückgibt, könnte sie auch genauso gut ein Array zurückgeben. Natürlich können Sie auch komplette Klassen beim Server anmelden und nicht nur einzelne Funktionen. Um eine Klasse anzumelden, nutzen Sie die Methode setClassO. Die Methoden, die darin enthalten sind, können dann vom Client auch direkt angesprochen werden, ohne dass der Name der Klasse angegeben werden muss. Das hat zur Folge, dass es schnell zu Namenskonflikten kommen kann. Um dies zu verhindern, kennen beide Methoden die Möglichkeit, einen Namespace zu nutzen. Dabei handelt es sich um einen einfachen String, den Sie als zweiten Parameter angeben. Wäre die Funktion aus dem letzten Beispiel mit dem Befehl $server->addFunction('hallo_sagen', 'stringverarbeitung ’); angemeldet worden, könnte der Client die Methode unter dem Namen stri ng- verarbeitung.hallo_sagen ansprechen. Ein besonders schönes Feature des Zend_Xml Rpc-Servers ist die Tatsache, dass drei »system-Methoden« implementiert sind. Hiermit kann der Client Informationen über den Server erhalten. Das heißt, der Client kann auf dem Server beispielsweise die Methode System. I 1 stMethods aufrufen. Der Server antwortet mit einer Liste der Methoden bzw. Funktionen, die bekannt sind. Die Methode System. method - Hel p bekommt vom Client den Namen einer Funktion oder Methode übergeben und liefert eine Erläuterung, was die Methode leistet. Mit system.methodSigna- ture kann der Client abfragen, was für einen Datentyp die Funktion zurückgibt und welche Parameter sie erwartet. Natürlich kann der Server nicht »hellsehen«, um diese Informationen zu ermitteln. Er kann aber einen DocBlock auslesen und die dort enthaltenen Informationen auswerten. Geben Sie einen solchen DocBlock nicht an, erkennt der Server die Anzahl der Parameter, die eine Funktion benötigt, kann aber die erwarteten Datentypen nicht ermitteln. Auch die Methode method - Help generiert die benötigten Informationen aus dem DocBlock. Bitte denken Sie bei der Erstellung des DocBlocks daran, dass die Informationen für Zend_Xml Rpc gedacht sind. Sie sollten also die XML-RPC-Datentypen verwenden. Wichtig dabei ist, dass Sie die Namen der Datentypen komplett in Kleinbuchstaben schreiben, andernfalls werden sie nicht erkannt. 357
e-bol.net 7 | Protokolle und Co. Fehlerbehandlung Ein weiteres pfiffiges Feature des Servers ist, dass er zwar auf Exceptions reagiert, die Meldungen aus den Exceptions aber nicht automatisch an den Client zurück- liefert. Damit ist sichergestellt, dass der Client keine Fehlermeldungen erhält, die eigentlich nicht für ihn gedacht sind. Damit Sie aber die Möglichkeit haben, bestimmte Fehler an den Client auszuge- ben, können Sie Exception-Klassen beim Server anmelden, deren Meldungen dann an den Client weitergegeben werden. Dazu müssen Sie die Klasse Zend_ Xml Rpc_Server_Faul t einbinden und den Namen der Exception-Klasse an die darin enthaltene statische Methode attachFaul tException() übergeben. Ein kompletter Server, der addieren und dividieren kann, könnte so aussehen: requi re_once 'Zend/XmlRpc/Server.php'; requi re_once 'Zend/XmlRpc/Server/Excepti on.php'; // Exception-Klasse fürs Error-Handling class RechnerException extends Exception 1 I class Rechner { * addiert zwei Zahlen * @param double Seins * @param double Szwei * @return double * / function addiere (Seins, Szwei) { return (Seins+Szwei): * Dividiert divident durch divisor * @param struct Szahlen * @return double * / function dividiere (Szahlen) { Sdivident = SzahlenE'divident']; Sdivisor = Szahlen['divisor']; 358
e-bol.net Nutzung von XAAL-RPC mit Zend_XmlRpc | 7-3 if (0 == $divisor) throw new RechnerException('Division durch Null'); 1 return ($divident / $divisor); } Sserver = new Zend_XmlRpc_Server(); Zend_XmlRpc_Server_Fault::attachFaultException( ’RechnerExcepti on'); // Anmelden der Klasse im Namespace "rechne" $server->setCIass('Rechner', 'rechne'); echo $server->handle(): Listing 7.14 Kompletter XML-RPC-Server Die beiden Methoden des Servers können jetzt nur mit den korrekten Datenty- pen aufgerufen werden. Versuchen Sie, die Methode dividiere beispielsweise mit einem Array und nicht mit einem Struct, also einem assoziativen Array, auf- zurufen, resultiert das in einer Fehlermeldung. Die Exception-Klasse, die hier deklariert ist, ist nicht sonderlich umfangreich. Sie wird lediglich benötigt, damit ein eindeutiger Klassenname vorhanden ist, der beim Server für die Fehlerbehandlung angemeldet werden kann. Sollten Sie einmal einen umfangreicheren Server implementieren, könnte es hilf- reich sein, die Klassen-Dateien zu cachen. Wie das funktioniert, können Sie dem Manual entnehmen. 7.3.3 Erstellen eines XML-RPC-Clients Der Client ist erfreulich schnell und einfach erstellt, da er nur aus einem Metho- denaufruf besteht, nämlich cal l (). Mit cal l () können Sie eine Methode auf dem Server aufrufen, die der Methode als erster Parameter übergeben wird. Die URL, unter der der Server zu erreichen ist, übergeben Sie dem Konstruktor der Klasse Zend_Xml Rpc_Server, welcher das Objekt bereitstellt, aus dem heraus Sie cal l () aufrufen. Ein Client, der den oben dargestellten Server anspricht, könnte folgendermaßen aussehen: 359
e-bol.net 7 | Protokolle und Co. requi re_once 'Zend_reposi tory/Xml Rpc/Cl ient.php’; // Objekt ableiten Sclient = new Zend_XmlRpc_Client( 'http://www.bei spiel.de/Server.php'); try { // Durchführen der Addition echo "Addition:<br>"; echo "1 + 2 = "; echo $cl ient->cal 1 (' rechne. addi ere ’, arrayd.O. 2.0)): echo ' <br>'; // Ausführen einer Division echo "<br>Division:<br>”; echo ”1 / 2 = ": $parameter = array(’divident'=>l, ’divisor’=>2): echo $client->cal1('rechne.dividiere’, array($parameter)); echo '<br>'; // Generiert einen Fehler: $parameter = array(’divident'=>l, ’divisor’=>0); echo $client->cal1('rechne.dividiere’, array($parameter)); I catch (Exception $e) { echo "Der folgende Fehler ist aufgetreten: ": echo $e->getMessage(): I Listing 7.15 XAAL-RPC-Client Wie Sie sehen, werden die für den Methodenaufruf benötigten Parameter als Array übergeben. Da die Addition Double-Werte erwartet, müssen hier auch Fließkomma-Werte übergeben werden, damit die automatische Konvertierung korrekt funktioniert. Die Division erfordert ein Struct. Damit dieses korrekt übergeben wird, muss zunächst ein assoziatives Array angelegt werden, welches die korrekten Schlüssel nutzt. Der Server kann die Schlüssel zwar nicht selbstständig prüfen, aber die Methode kann die Werte sonst nicht auslesen. Dieses assoziative Array muss dann wieder in ein Array verpackt werden, damit es korrekt übergeben wird. Die Ausgabe, die von diesem Client generiert wird, lautet: Addition: 1 + 2 = 3 360
e-bol.net Nutzung von REST mit Zend_Rest | 7-4 Division: 172=0.5 Der folgende Fehler ist aufgetreten: Division durch Null Auch die Fehlermeldung wird korrekt übermittelt. Nutzen Sie einen fremden XML-RPC-Server, kann es oft hilfreich sein zu sehen, welche Anfrage Ihr Client verschickt hat. Das können Sie ohne Probleme in das Exception Handling inte- grieren. Und zwar können Sie die letzte Anfrage mit getl_astRequest() auslesen. Die folgenden Zeilen könnten also beim Debugging hilfreich sein: echo "Debug-Information:<br>"; echo "Request:<br>"; echo nl2br(htmlspeci alchars($ c11 ent->getLa stRequest())): 7.4 Nutzung von REST mit Zend_Rest Die Abkürzung REST steht für »Representational State Transfer« und bezeichnet eine weitere wichtige Variante, um Webservices zu implementieren. Etwas unge- wöhnlich bei REST ist allerdings, dass es sich nicht um einen genau definierten Standard handelt, sondern vielmehr um die Beschreibung einer Architektur. Das heißt, dass Client und Server recht genau wissen müssen, wie sie miteinander zu kommunizieren haben. REST basiert auf den »üblichen« Standards. Es werden also üblicherweise XML-Daten über das HTTP-Protokoll ausgetauscht. Grundsätz- lich unterstützt REST die vier HTTP-Befehle GET, POST, DELETE und PUT, die sich auch als Methoden in der Klasse wiederfinden. In den meisten Fällen findet die Datenübergabe bei REST mit der GET-Methode statt, sodass die Daten im Endef- fekt einfach an die URL angehängt werden. Nachfolgend werde ich auf die API von Yahool-Maps zugreifen. Bei Yahoo! kann eine solche URL, die auch als API- Endpunkt bezeichnet wird, so aussehen: http://local.yahooapis.com/MapsService/V1/geocode Im Fall von Yahoo! folgt nach dem Host (local.yahooapis.com) der Servicename (MapsService) und die Versionsnummer (V7). Das ist der übliche Aufbau, obwohl die Struktur des API-Endpunktes natürlich frei gewählt werden kann. Der letzte Teil der URL (geocode) bezeichnet in diesem Fall die Methode, die genutzt wer- den soll. Üblicherweise werden der Methode noch Werte übergeben, wie Sie gleich sehen werden. 361
e-bol.net 7 | Protokolle und Co. 7.4.1 Zugriff auf offene REST-Schnittstellen Einen REST-Client mit Zend_Rest zu erstellen, ist denkbar einfach. Allerdings gilt es dabei, zwei Fälle zu unterscheiden. Nutzen Sie einen Server, der auf Zend_Rest aufbaut oder nutzen Sie keinen? Im ersten Fall haben Sie ein paar mehr Möglich- keiten. Ich werde Ihnen aber zuerst den Zugriff auf ein anderes Angebot, nämlich auf Yahoo!-Maps, vorstellen. Um einen Client zu erhalten, müssen Sie im Prinzip nur ein Objekt der Klasse Zend_Rest_Cl i ent ableiten. Beim Ableiten des Objekts können Sie dem Kon- struktor direkt den API-Endpunkt übergeben. Alternativ können Sie den End- punkt auch später mit der Methode setllri () an das Objekt übergeben. Die weitere Vorgehensweise hängt nun allerdings von dem Dienst ab, den Sie ansprechen wollen. Das heißt, Sie müssen sich mit der Dokumentation des ent- sprechenden Anbieters befassen. Im folgenden Beispiel sollen mithilfe der Yahoo!-Maps-API der Längen- und der Breitengrad einer Adresse ermittelt wer- den. Die Dokumentation zu diesem API-Aufruf finden Sie unter http://developer. yahooxom/maps/rest/Vl/geocode.html. Die Dokumentation besagt, dass der Methode geocode bestimmte Informationen mit übergeben werden müssen, damit sie ihre Aufgabe erfüllen kann. Das ist einleuchtend, denn woher sollte die Methode sonst wissen, um welche Adresse es sich handelt. Darüber hinaus benö- tigt die Methode noch Ihren API-Key, mit dem Sie sich identifizieren. Nachfol- gend wird nur ein Beispiel-Key genutzt, welchen Sie für den Produktivbetrieb nicht benutzen sollten. Der Key muss als Parameter appi d übergeben werden und die einzelnen Bestand- teile der Adresse mithilfe der Parameter Street und city. Eine etwas unschöne Eigenschaft der API ist, dass deutsche Sonderzeichen vermieden werden sollten. Das bedeutet, dass ein »ß« durch »ss« ersetzt werden muss, »ä« durch »ae« usw. In diesem Fall müssen Sie sich also keine Gedanken um den Zeichensatz oder die Codierung machen. Ansonsten gibt Zend_Rest die Daten allerdings in dem Zei- chensatz weiter, in dem sie übergeben wurden, wobei die Klasse die Daten natür- lich korrekt URL-codiert. Kurioserweise enthält die Antwort wieder Umlaute in UTF-8-Codierung. Um die Parameter zu setzen, rufen Sie einfach eine Methode auf, die den Namen des Parameters hat, und übergeben ihr den Wert, der genutzt werden soll. Nach- dem das erfolgt ist, können Sie die gewünschte Methode auf dem Server aufru- fen, indem Sie eine der Methoden get(), post(), del ete() oder put() verwen- den. Jede entspricht dabei dem entsprechenden HTTP-Befehl. Üblich ist, dass die Daten per GET übergeben werden. Dies entspricht dem, was die Yahoo!-API erwartet: 362
e-bol.net Nutzung von REST mit Zend_Rest | 7-4 require_once ' Zend/Rest/CI lent.php’; $client = new Zend_Rest_Cl1ent( 'http://local.yahooapis.com/MapsService/Vl/geocode'); $client->appid(’YahooDemo'): $client->street(’Rhoenstrasse'); $client->city('Bielefeld'): $erg = $c1i ent->get(): echo "Längengrad$erg->Result->Longi tude."<br>"; echo "Breitengrad:",$erg->Result->Latitude. ’’<br>"; echo "Straße: ".utf8_decode($erg->Result->Address."<br>"); echo "Ort: ".utf8_decode($erg->Result->C1ty."<br>"); Listing 7.16 Zugriff auf Yahoo!-Maps Der Aufruf der Methode get () sendet die Anfrage an den Server und liefert die Antwort als SimpleXML-Objekt zurück. Dieses kann dann mit den in PHP übli- chen Methoden verarbeitet werden. Auch dies setzt voraus, dass Sie genau wis- sen, wie die Antwort des Servers aufgebaut ist. Hierbei ist zu beachten, dass Zend_Rest zurzeit nur mit Antworten umgehen kann, die ein gültiges XML- Dokument darstellen. Somit kann Yahoos! Feature, direkt serialisiertes PHP zurückzuliefern, mit diesem Client nicht genutzt werden. Die Ausgabe von Listing 7.16 sehen Sie in Abbildung 7.5. O n http://127.0.0.1/~c.../zf-buch/rest/l.php 0 G - Q7 Google » £Q Planet PHP (17) Apple (9) ▼ Amazon eBay » Längcngrad:8.603196 Breitengrad :52.020036 Straße: Rhönstrasse Ort: 33719 Bielefeld Abbildung 7.5 Ausgabe der geodätischen Informationen 7.4.2 Implementation eines REST-Servers Wie schon erwähnt, läuft Zend_Rest erst dann zu voller Leistungsfähigkeit auf, wenn Client und Server kombiniert werden. Das resultiert daraus, dass der Ser- 363
e-bol.net 7 | Protokolle und Co. ver noch einige zusätzliche Information mitliefert, die seitens des Clients verwer- tet werden können. Möchten Sie einen REST-Server auf Basis von Zend_Rest implementieren, so ver- fügen Sie über zwei Möglichkeiten, Funktionalitäten zu hinterlegen. Die erste Methode ist, Funktionen zu nutzen. Diese müssen Sie, nachdem Sie ein Server- Objekt abgeleitet haben, mithilfe von addFunction() anmelden. Die Methode bekommt dabei den Namen der Funktion als Parameter übergeben. Möchten Sie mehrere Funktionen auf einmal anmelden, so übergeben Sie diese einfach als indiziertes Array. Die zweite Möglichkeit ist, eine Klasse beim Server anzumel- den. Dazu übergeben Sie den Namen der Klasse an die Methode setClass(). Der Server erkennt dann selbstständig, welche Methoden in der Klasse enthalten sind. Benötigt der Konstruktor der übergebenen Klasse Parameter, übergeben Sie diese in Form eines Arrays als dritten Parameter. Der zweite Parameter wird im nächsten Absatz erläutert. Sie können beide Methoden mehrfach aufrufen. Kommt es bei den Funktionen oder Methoden in den Klassen zu Namensgleichheiten, wird jeweils diejenige Funktion bzw. Methode genutzt, die zuletzt übergeben wurde. Zwar unterstüt- zen die beiden Methoden zum Anmelden der Klassen einen zweiten Parameter für einen Namespace, aber dieser wird intern nicht genutzt. Nachdem Sie die Funktionen oder Klassen angemeldet haben, rufen Sie die Methode handlet) auf. - Ihr Server ist nun fertig für den Einsatz. Es ist nun noch zu klären, wie die Funktionen bzw. Methoden aufgebaut sein müssen, die Sie nutzen wollen. Im einfachsten Fall können Sie das Ergebnis der Verarbeitung direkt mit return zurückgeben, wie das folgende Beispiel zeigt: require_once ’Zend/Rest/Server.php’; class Rechner { // Addiert zwei Werte public function addiere ($summand_l, $summand_2) { return (float) $summand_l + (float) $summand_2; // Subtrahiert den zweiten Wert vom ersten public function subtrahiere (Sminuend, tsubtrahend) { return (float) $mi nuend - (float) $subtnahend: 1 364
e-bol.net Nutzung von REST mit Zend_Rest | 7-4 // Neues Server-Objekt ableiten Sserver = new Zend_Rest_Server(); // Klasse mit den Methoden anmelden $server->setCIass('Rechner'); // Server "starten" $server->handle(); Listing 7.17 Aufbau eines REST-Servers Um den Server anzusprechen, rufen Sie die URL direkt auf, wobei Sie die benöti- gen Parameter über die URL übergeben können. Hierbei bietet es sich an, den Server in einem Verzeichnis abzulegen und die Datei index.php zu nennen. Bei einer komplexeren Anwendung empfiehlt sich natürlich die Applikation als MVC aufzubauen, wobei Sie den Server dann im Action-Controller initialisieren könn- ten. Welche Methode aufgerufen wird, definieren Sie hierbei über den Parame- ter method, dem der Name der gewünschten Methode übergeben wird. Die Para- meter, die Sie der Methode übergeben wollen, können Sie auf zwei Arten spezifizieren. Die Server-Klasse analysiert die Methoden und Funktionen und erfasst dabei die Namen der Parameter, sodass Sie die Namen der Parameter direkt nutzen können. Als zweite Variante übergeben Sie die Parameter der Reihe nach. Damit der Server die Argumente eindeutig erkennt, müssen Sie diese argO, arg1, arg2, arg3 usw. benennen, wobei sich die Reihenfolge aus der Nummerie- rung und nicht aus der tatsächlichen Reihenfolge in der URL ergibt. Wollen Sie 3 von 4 subtrahieren, so könnten Sie diese www.zf-buch.de/rest/?method=subtrahi ere&argl=4&arg2=3 oder diese URL www.zf-buch.de/rest/?method=subtrahi ere&subtrahend=3&minuend=4 für den Aufruf nutzen. Die XML-Struktur der Antwort sieht folgendermaßen aus: <?xml version="l.0" encoding="UTF-8"?> <Rechner generator="zend" version="l,0"> <subtrahi ere> <response>l</response> <status>success</status> </subtrahi ere> </Rechner> In der XML-Antwort sind die Daten also gekapselt enthalten. Das Root-Element Rechner entspricht dem Namen der Klasse, danach folgt der Name der Methode und zum Schluss als Wert des Elements response der Rückgabewert der Funk- 365
e-bol.net 7 | Protokolle und Co. tion. Mit dem Element Status können Sie dem Client mitteilen, ob der Aufruf erfolgreich war. Sie werden dies gleich noch sehen. Da Sie den Server wahrscheinlich nicht direkt ansprechen, sondern eher den Cli- ent des Pakets nutzen wollen, könnte der Aufruf wie folgt aussehen: require_once 'Zend/Rest/Client.php'; Sclient = new Zend_Rest_Cl1ent('http://www.zf-buch.de/rest'): $cl1ent->method('subtrahiere’); $client->minuend(4); $client->subtrahend(3); $erg = $client->get(); echo "Ergebnis der Subtraktion: ",$erg->subtrahiere->response; Die meisten Punkte kommen Ihnen sicher bekannt vor. Die Methode get() kön- nen Sie übrigens auch durch post() ersetzen. Neu in diesem Beispiel ist, dass mit der Methode method() definiert wird, welche der Methoden auf dem Server angesprochen werden soll. Die Kombination aus Zend-Server und -Client gestattet Ihnen aber noch eine zweite Variante: require_once 'Zend/Rest/Client.php'; Sclient = new Zend_Rest_Client('http://www.zf-buch.de/rest'): // Deklaration der Methode und der Parameter $client->subtrahiere(4, 3): $erg = $client->get(); echo "Ergebnis der Subtraktion: ",$erg->subtrahiere->response; Durch den Aufruf $client->subtrahiere(4,3): wird festgelegt, welche Methode genutzt werden soll und welche Parameter diese erhalten soll. Bei die- ser Vorgehensweise werden die Parameter mithilfe von argO, argl etc. überge- ben. In dem vorangegangenen Beispiel hatten die Methoden nur einen Rückgabewert. Möchten Sie mehrere Werte zurückgeben, ist das auch kein Problem. Liefern Sie dazu einfach ein assoziatives Array mit Daten zurück. Die Schlüssel des Arrays werden dabei als Elementnamen in der XML-Antwort übernommen. Wenn also die Methode subtrahiere auch die übergebenen Parameter mit an den Client schicken soll, so könnten Sie das so umsetzen: 366
e-bol.net Nutzung von REST mit Zend_Rest | 7-4 public function subtrahiere ($minuend. $subtrahend) { return array('minuend'=>$minuend. 'Subtrahend'=>$Subtrahend, ’ergebnis’ => ((float) $minuend - (float) $subtrahend)); } Der XML-Code der Antwort würde nun so aussehen: <?xml version="l.0" encoding="UTF-8"?> <Rechner generator="zend" version="l.0"> <subtrahi ere> <mi nuend>4</minuend> <subtrahend>3</subtrahend> <ergebnis>l</ergebnis> <status>success</status> </subtrahiere> </Rechner> Den Inhalt des Elements Status können Sie auch beeinflussen. Bei diesem Ele- ment handelt es sich um ein Feature der Zend_Rest-Klassen. Hiermit kann der Server dem Client mitteilen, ob eine Operation erfolgreich war. Geben Sie ein Array zurück, bei dem der Schlüssel Status den Wert false enthält, erkennt der Client, dass ein Fehlschlag vorliegt. Clientseitig können Sie mit den Methoden isSuccess() bzw. isError() abfragen, ob die Operation erfolgreich war. Das folgende Beispiel zeigt, wie es funktioniert. Hierbei wird der Klasse Rechner eine neue Methode für die Division hinzugefügt. Da eine Division durch 0 nicht definiert ist, liefert die Methode eine Fehlermeldung zurück, falls der Divisor 0 ist: public function dividiere ($dividend, Sdivisor) { if (0 == $divisor) { return array ('meldung'=> 'Division durch Null ist nicht definiert', 'Status' => false): el se { return array(’ergebnis'=> ((float) $dividend / (float) $divisor)); } 367
e-bol.net 7 | Protokolle und Co. Und hier das dazugehörige clientseitige Script: require_once ’Zend/Rest/Client.php’; Sclient = new Zend_Rest_Client('http://www.zf-buch.de/rest'): $client->dividiere(4, 0): $erg = $client->get(); if ($erg->isSuccess()) { echo "Ergebnis der Division: ".$erg->dividiere->ergebnis: } el se { echo "Fehler! Meldung: ".$erg->dividiere->meldung; } 368
e-bol.net There is a difference between knowing the path and walking the path. - Morpheus, Matrix 8 Lokalisierung und Internationalisierung Webprojekte werden zunehmend international. Viele Webseiten müssen heutzu- tage mindestens zweisprachig sein. Wenn ich hier nur Sprachen erwähne, dann ist das natürlich nur die halbe Wahrheit. Dazu kommt, dass Datums- und Zahlen- Formate angepasst werden müssen, unterschiedliche Feiertage existieren, Länder in anderen Zeitzonen liegen und vieles Weiteres. Bei diesen Anpassungen spricht man von Internationalisierung bzw. Lokalisierung. Sollten Sie mal über die Abkürzungen II8N oder LION stolpern, bedeuten diese nichts anderes als »Inter- nationalization« bzw. »Localization«. Die Zahlen geben dabei an, wie viele Zei- chen ausgelassen wurden, und bei den Buchstaben handelt es sich jeweils um den ersten und den letzten Buchstaben des Wortes. Dieses Thema kann sehr schnell sehr komplex werden, wie Sie vielleicht schon vermuten. Aber auch hier liefert das Zend Framework einige Klassen, die Ihnen viel Arbeit abnehmen. 8.1 Lokalisierung mit Zend_Locale Basis für die Nutzung vieler Lokalisierungs-Klassen ist die Klasse Zend_Locale. Mit ihr legen Sie fest, für welchen Sprachraum bzw. für welche Region lokalisiert werden soll. Es fragt sich, wie Sie der Klasse mitteilen, welche Lokalisierung genutzt werden soll. Dafür gibt es sogenannte Locales. Ein Locale definiert, welche Sprache in welcher Regionalisierung genutzt werden soll. Der Locale für Deutschland wäre beispielsweise de_DE. Das bedeutet, dass die deutsche Sprache benutzt werden soll (de) und es sich um die Region Deutschland (DE) handelt. Das hört sich viel- leicht ein wenig komisch an, ist aber durchaus sinnvoll. Denken Sie zum Beispiel an Österreich. Auch dort wird Deutsch gesprochen, aber ein paar kleine Details sind dann doch anders. So heißt der erste Monat des Jahres in Österreich nicht Januar, sondern Jänner. Daher hat Österreich einen eigenen Locale, der de_AT 369
e-bol.net 8 | Lokalisierung und Internationalisierung lautet. Ähnliches gilt beispielsweise auch für England (en_UK) und die USA (en_ US). Zwar haben beide Länder grundsätzlich dieselbe Sprache, aber es bestehen doch Unterschiede. Die beiden Landeskürzel, aus denen sich ein Locale zusammensetzt, sind von der ISO definiert. Eine Liste der möglichen und gültigen Kombinationen finden Sie beim Unicode-Konsortium.1 Diese Locales definieren allerdings nicht nur die Sprache, sondern alles, was für einen bestimmten Kulturraum üblich ist, wie zum Beispiel Zahlenformate und Währungen. Die Landeseinstellungen, die Sie nutzen möchten, können Sie direkt beim Ablei- ten des Objekts angeben oder später über die Methode setl_ocale() setzen: requi re_once('Zend/Locale.php'); // Direkt setzen Slocale = new Zend_Locale(’de_DE'): // Erst ableiten und dann setzen Slocale = new Zend_Locale(); $locale->setLocale(’de_DE'); Es stellt sich die Frage, welche Lokalisierung der Benutzer erwartet. Natürlich können Sie diese vorschreiben, aber Sie können auch den Browser »fragen«. Ist der Browser korrekt konfiguriert, so teilt er dem Server mit, welche Sprachen bzw. Lokalisierungen akzeptiert werden. Übergeben Sie dem Konstruktor keinen Locale, so wird die Default-Einstellung vom Browser übernommen. Da Ihre Anwendung wahrscheinlich nicht alle Sprachen der Welt unterstützt, sollten Sie überprüfen, ob die automatisch eingestellte Lokalisierung genutzt werden kann. Slocale = new Zend_Locale(); $lang = strtolower($1ocale->getLanguage()): if ($lang != 'de' ) { $locale->setLocale(’en_US'); 1 Hier wird mithilfe von getLanguage() die aktuelle Einstellung ausgelesen. Ist die automatisch ermittelte Sprache nicht Deutsch, wird amerikanisches Englisch selektiert. Sollte der Browser noch andere Sprachen akzeptieren, so können Sie diese mit der Methode getBrowserf) auslesen, welche die erlaubten Locales als Array zurückliefert. In diesem Zusammenhang kann es auch hilfreich sein zu prü- 1 http://unicode.org/cldr/data/diff/supplemental/languages_and_territories.html 370
e-bol.net Lokalisierung mit Zend_Locale | 8.1 fen, welche Zeichensätze der Browser unterstützt. Diese Information erhalten Sie von der Methode getHttpCharsett) in Form eines Arrays. 8.1.1 Standardtexte und Standardformate lokalisieren Zend_Local e ist aber nicht nur dafür da, eine Lokalisierungsinformation für eine andere Klasse bereitzustellen. Die Klasse kann selbst auch schon einiges leisten. Standardtexte Neben vielen landesspezifischen Informationen zur Formatierung von Datums- und Währungsangaben stellt die Klasse auch gleich Übersetzungen von häufig gebrauchten Wörtern zur Verfügung. Diese Informationen können Sie mit der Methode getTrans l at1onL1st() auslesen. Sie bekommt einen String übergeben, der ihr mitteilt, welche Information Sie benötigen. Als zweiten Parameter kön- nen Sie noch einen weiteren Locale angeben, wenn Sie eine andere Sprache benötigen als die momentan gesetzte. Als Rückgabewert erhalten Sie ein Array mit der gewünschten Information. Wollten Sie beispielsweise den russischen Namen für Deutsch und Englisch haben, könnten Sie das so machen: header(’Content-Type: text/html; charset=utf-8’); require_once('Zend/Locale.php'); $locale = new Zend_Locale('ru_RU'); {deutsch = $locale->getTranslationList(’Language' , ’de_DE'); {sprachen = array_flip($deutsch); {russisch = {1ocale->getTranslationList('Language'); {schluessel = {sprachen['Deutsch*]; echo "Deutsch heißt auf Russisch 'Srussisch[{schluessel]'<br>"; {schluessel = {sprachen[' Engiisch']; echo "Englisch heißt auf Russisch ’{russisch[{schluessel; Listing 8.1 Ausgabe von lokalisierten Sprachinformationen O http;//127.0.0.1...alisierung/2.php GD Deutsch heißt auf Russisch ’hcmcukhä' Englisch heißt auf Russisch 'ainvnröcKJuY Abbildung 8.1 Ausgabe übersetzter Texte 371
e-bol.net 8 | Lokalisierung und Internationalisierung Der Code sieht auf den ersten Blick vielleicht etwas ungewöhnlich aus. Ich habe mithilfe von getTranslationList(' Language', ’de_DE’) zuerst die deutschen Übersetzungen ausgelesen. Dabei wird ein Array zurückgeliefert, das wie folgt aufgebaut ist: array(477) { ["aa"]=> string(4) "Afar" ["ab"]=> string(lO) "Abchasisch" // 475 weitere Einträge Die Funktion array_f 1 ip() vertauscht die Schlüssel und die Werte, sodass $sprachen[' Deutsch' ]; den ursprünglichen Schlüssel für »Deutsch« zurückgibt. Da diese Schlüssel konsistent für alle Sprachen genutzt werden, kann damit jetzt auch die Übersetzung aus dem »russischen Array« ausgelesen werden. Neben dem Parameter 'Language' unterstützt getTranslatonList() noch wei- tere Parameter. Sie finden die wichtigsten in Tabelle 8.1. Parameter Bedeutung Language Liste der Sprachen Country Liste der Ländernamen Terri tory Liste mit Gebietsnamen (Südasien, Westeuropa, Osteuropa usw.) Month lokalisierte Monatsnamen Month_short abgekürzte Monatsnamen (zwei bis vier Buchstaben) Month_narrow abgekürzte Monatsnamen (meist ein Buchstabe) Day übersetzte Namen der Tage Day_short abgekürzte Tagesnamen (zwei bis vier Zeichen) Day_narrow gekürzter Name des Tages (meist ein Zeichen) Dateformat Datums-Format-Strings für die Nutzung mit date() Timeformat Zeit-Format-Strings für die Nutzung mit date() Timezone übersetzte Liste der Zeitzone(n) des Landes mit Ortsangabe Currency lokalisierte Liste mit Währungen Characters Liste mit denjenigen Buchstaben, die in der Region genutzt werden Tabelle 8.1 Parameter für getTranslationListO Die Liste aus Tabelle 8.1 ist nicht ganz vollständig. Eine komplette Liste finden Sie in der Dokumentation des Zend Frameworks.2 2 http://framework.zend.com/manual/de/zend.locale.functions.html 372
e-bol.net Lokalisierung mit Zend_Locale | 8.1 Bei den Parametern Dateformat, Timeformat und Characters ist es natürlich nicht sinnvoll, mit dem oben vorgestellten array_flip() zu arbeiten. Schauen Sie sich das Array am besten kurz mit var_dump() oder print_r() an, um zu erkennen, welche Schlüssel dort zurückgegeben werden. Ich möchte Ihnen nicht verschweigen, dass auch einige »Convenience-Funktio- nen« definiert sind, wie getLanguageTranslation(). Diese leisten dasselbe wie getTranslationl_ist(), benötigen aber keinen Parameter. Auch diese Methoden werden in der Dokumentation erläutert. Ähnlich wie die bereits besprochenen Methoden verhält sich auch getQues- tionO. Sie liefert die Übersetzungen für »Ja« und »Nein« zurück, wobei die Array-Schlüssel 'yes' und ’no’ sind. Für die deutsche Übersetzung ist also 'yes' => 'ja' und ' no' => ’nein’ enthalten. Darüber hinaus finden Sie auch noch die jeweiligen Abkürzungen unter den Schlüsseln ’yesabbr’ und ’noabbr’. Für Deutschland würde es sich dabei um j und n handeln.3 Zahlen Die Arbeit mit Zahlen kann bei der Lokalisierung von Applikationen recht auf- wändig werden. Wollten Sie die Zahl 1234,10 korrekt formatieren, so wäre 1.234,12 die korrekte deutsche Schreibweise. In einem englischsprachigen Land würde man allerdings die Formatierung 1,234.10 erwarten. Und wenn Sie die Zahl für einen Liechtensteiner korrekt darstellen wollen, müssten Sie mit Hoch- kommas und Dezimalpunkten arbeiten: 1'234.10. Um diese verschiedenen Formate nicht zum Problem werden zu lassen, sind in der Klasse Zend_Local e_Format verschiedene statische Methoden definiert, die Sie unterstützen. Die Klasse wird automatisch mit eingebunden, wenn Sie Zend_ Local e inkludieren. Die Zahlen können grundsätzlich in beide »Richtungen« konvertiert werden. Um zum Beispiel eine Fließkommazahl in eine lokalisierte Darstellung zu bringen, könnten Sie die folgenden Zeilen nutzen: header('Content-Type: text/html; charset=utf-8'): requi re_once('Zend/Locale.php'); requi re_once('Zend/Locale/Format.php'); // Locales ableiten $locale_de = new Zend_Locale(’de_DE'): $locale_us = new Zend_Locale(’en_US'): 3 Die Dokumentation erwähnt noch zwei weitere Felder mit regulären Ausdrücken zum Prüfen von Eingaben. Momentan (Version 1.0.3) sind diese allerdings nicht enthalten. 373
e-bol.net 8 | Lokalisierung und Internationalisierung $zahl = 12345.1; //Optionen zusammenstel1 en Soptionen = array('1ocale'=>$1ocale_de, 'precision'=>2); echo "Deutsche Formatierung: echo Zend_Locale_Format::toFloat($zahl, $optionen); // Ausgabe: Deutsche Formatierung: 12.345.10 //Locale neu setzen $optionen[11ocale' ] = $locale_us; $zahl = *12345.678' ; echo "CbOAmerikanisehe Formatierung: echo Zend_Locale_Format::toFloat($zahl, $optionen); //Ausgabe: Amerikanische Formatierung: 12.345.68 Die Methode toFl oat() bekommt als ersten Parameter die Zahl übergeben, die formatiert werden soll. Diese kann als Zahl (erstes Beispiel) oder als String (wie im zweiten Beispiel) übergeben werden. Als zweiten Parameter erwartet die Methode ein Array mit Optionen. Die Lokalisierung wird mithilfe eines Zend_ Locale-Objekts angegeben, das als Wert des Schlüssels locale übergeben wird. Alternativ können Sie an dieser Stelle auch einen String wie ’de’ übergeben. Optional können Sie mithilfe des Schlüssels precision noch angeben, wie viele Nachkommastellen genutzt werden sollen. Geben Sie diese nicht an, dann gibt die Methode genau so viele Nachkommastellen aus, wie die übergebene Zahl hat. Hat die Zahl mehr Nachkommastellen als der Wert, der in preci si on angegeben ist, wird die Zahl auf die angegebene Anzahl der Stellen gerundet. Ist preci si on größer als die Anzahl der vorhandenen Stellen, so werden Nullen ergänzt. Für die Konvertierung von Fließkommazahlen ist darüber hinaus noch eine wei- tere Methode vorgesehen: toNumberO. Sie wird im Hintergrund auch von toF1 oat() genutzt, kann aber noch mehr. Und zwar können Sie toNumber() auch eine Formatierungsanweisung mitgeben, wohingegen toFloatO immer die Standardformatierung nutzt, die zu dem Locale gehört. Eine solche Formatanwei- sung können Sie mithilfe eines Strings definieren, der im Optionen-Array als Wert des Schlüssels number_format übergeben wird. Bei einer solchen Formatangabe steht die Zahl Null für den Vorkomma-Anteil bzw. die Einer-Stelle des Vorkommaanteils. Möchten Sie beim Vorkomma-Anteil Trennzeichen verwenden, können Sie diese mit einem Komma symbolisieren. Dabei stellt sich die Frage, wo dieses Trennzeichen erscheinen soll, ob nach der Einer-, der Zehner- oder der Hunderter-Stelle. Um dieses zu definieren, nutzen Sie das Doppelkreuz, das für eine weitere Stelle steht. Das Trennzeichen selbst muss durch ein Komma symbolisiert werden und wird dann später durch das 374
e-bol.net Lokalisierung mit Zend_Locale | 8.1 Zeichen ersetzt, welches in der Lokalisierung korrekt ist. Um die Anzahl der Stel- len für den Nachkommanteil anzugeben, geben Sie pro Stelle einfach eine Null an, die mit einem Punkt von der »Vorkomma-Null« getrennt wird. requi re_once('Zend/Locale/Format.php'); $locale_de = new Zend_Locale('de_DE'); $optionen=array('locale'=>$locale_de); $zahl = 12345678.5555; // Ausgabe ohne Nachkommastellen $optionenE'number_format'] = '0'; echo Zend_Locale_Format::toNumber($zahl, $optionen); //Ausgabe: 12345679 echo "<br>"; // Ausgabe mit 2 Nachkommastellen $optionenE'number_format'] = '0.00'; echo Zend_Locale_Format::toNumber($zahl, $optionen); // Ausgabe: 123456789,56 echo "<br>"; // 2 Nachkommastellen und "Tausender-Trennzeichen" // nach jeder Vorkommastelle $optionenE'number_format'] = '#,0.00'; echo Zend_Locale_Format::toNumber($zahl, $optionen); // Ausgabe: 1.2.3.4.5.6.7.8,56 echo "<br>"; // 2 Nachkommastellen und "Tausender-Trennzeichen" // nach jeder zweiten Vorkommastelle $optionenE'number_format'] = '#,#0.00'; echo Zend_Locale_Format::toNumber($zahl, $optionen); // Ausgabe: 12.34.56.78,56 echo "<br>"; // 2 Nachkommastellen und "Tausender-Trennzeichen" // nach jeder dritten Vorkommastelle $optionenE'number_format'] = '#,##0.00'; echo Zend_Locale_Format::toNumber($zahl, $optionen); // Ausgabe: 12.345.678,56 echo "<br>"; Listing 8.2 Formatieren von Zahlen 375
e-bol.net 8 | Lokalisierung und Internationalisierung Die Kombination von number_format und preci sion sollten Sie momentan noch vermeiden, da sie unter Umständen zu einer fehlerhaften Darstellung führt. n http:/., g/4.php CD m u © » 12345679 12345678,56 12.3.4.5.6.7.8,56 12.34.56.78,56 12.345.678,56 Abbildung 8. 2 Ausgabe formatierter Zahlen Neben toNumberO und toFloatO gibt es übrigens auch noch die Methode tolnteger(). Diese verhält sich wie die beiden erläuterten Funktionen, entfernt den Nachkommateil, rundet dabei aber. Außerdem werden die Tausender-Trenn- zeichen des gewählten Locales ergänzt. Eine manuelle Formatierung wird nicht unterstützt. $locale_de = new Zend_Locale('de_DE’): $optionen=array('1ocale’=>$1ocale_de): $zahl = 12345678.5555: echo Zend_Locale_Format::tolnteger!$zahl, Soptionen); //Ausgabe: 12.345.679 Wie schon erwähnt, können Sie aber auch Zahlen einlesen, die in einer lokalisier- ten Schreibweise eingegeben wurden. Hierzu sind getNumber(), getFloat!) und getInteger!) definiert. Diese Methoden bekommen an ersten Stelle denjenigen String übergeben, der in eine Zahl konvertiert werden soll. Als zweiter Parameter folgt auch hier wieder ein Array mit Parametern, wobei auch ein Zend_Local e- Objekt übergeben werden sollte. Die beiden ersten Methoden unterstützen dar- über hinaus auch den Schlüssel precision. Sollten Sie Werte verarbeiten, die über ein Formular eingegeben wurden, ist zu ermitteln, ob der Benutzer die Zahl korrekt formatiert eingegeben hat. Das können Sie mit den Methoden isInte- ger!), i sNumber!) und i sFl oat!) prüfen. Diese Methoden bekommen an erster Stelle den String übergeben, der geprüft werden soll, und an zweiter Stelle ein Array, welches als Wert des Schlüssels locale ein Zend_Locale-Objekt enthält. Ein wenig kurios mag an dieser Stelle anmuten, dass ein String wie ' 13,445.1 ’ oder '13.445,1' von der Methode islnteger!) als Integer-Wert erkannt wird. Der Hintergrund ist, dass hier ein normalisierter Integer-Wert (13445) enthalten ist. Da ein Integerwert wie 12345 auch immer als Fließkommawert interpretiert werden kann, liefern die drei Methoden zurzeit das gleiche Ergebnis. 376
e-bol.net Lokalisierung mit Zend_Locale | 8.1 Interessanter sind da schon die Methoden getNumbert), getFloat() und getln- teger(), welche eine lokalisierte Zahl übergeben bekommen und diese in eine normalisierte PHP-Darstellung bringen. Die beiden Methoden getNumber() und getFl oat() unterscheiden sich dabei durch ihren Datentyp. Die erste gibt einen String zurück, wohingegen die zweite einen Float-Wert liefert: require_once ' Zend/Locale/Format.php': $locale_de = new Zend_Locale(’de_DE'); $opti onen=array(' l ocale’=>$locale_de. 'preci si on'=>2); $zahl = "123.456.555": // Ausgabe mit zwei $erg = Zend_Locale_Format::getNumber($zahl, $optionen): echo $erg." ist vom Typ "; echo gettype($erg); echo "<br>"; // Ausgabe mit zwei $erg = Zend_Locale_Format::getFloat($zahl. Soptionen); echo $erg.” ist vom Typ echo gettypet $erg); echo "<br>"; // Ausgabe ohne Nachkommastallen $erg = Zend_Locale_Format::getlnteger($zahl, $optionen); echo $erg.” ist vom Typ echo gettypet $erg); echo "<br>"; Wie Sie in Abbildung 8.3 sehen können, beachten die Methoden auch die Option precision. O http://l...ung/S.php GD w . u u 12345655 ist vom Typ string 123456.55 ist vom Typ double 123456 ist vom Typ integer Abbildung 8. 3 Eingelesene und konvertierte Werte 377
e-bol.net 8 | Lokalisierung und Internationalisierung Zudem ist es auch noch möglich, die Zahlen in verschiedenen Zahlensystemen wie beispielsweise dem ostarabischen darzustellen. Informationen dazu finden Sie in der Dokumentation. Datums- und Zeitangaben Da die Angabe eines Datums oder einer Uhrzeit in verschiedenen Ländern unter- schiedlich gehandhabt wird, kann es recht umständlich sein, Datumsangaben oder Uhrzeiten einzulesen, um damit zu rechnen. Möchten Sie mit einem lokalisierten Datum arbeiten und dieses in seine Bestand- teile zerlegen, ist die statische Methode getDate() sehr hilfreich. Die Methode bekommt als ersten Parameter einen String übergeben, der das zu analysierende Datum enthält. Der zweite Parameter ist ein Array, mit dem Sie diverse Informa- tionen übergeben können. Im einfachsten Fall übergeben Sie mit dem Schlüssel locale ein Locale-Objekt. Aus diesem liest die Methode den »üblichen« Aufbau des Datums für die Region aus und liefert die einzelnen Datumsbestandteile als Array zurück: requi re_once ’Zend/Locale/Format.php'; Slocale = new Zend_Locale('de_DE'): Sdatum = '10.03.1970’ : Steile = Zend_Locale_Format::getDate(Sdatum, array('locale'=>Slocale)); echo "Datumsformat: ".Zend_Locale_Format::getDateFormat($1ocale); echo "CbODatum: Sdatum<br>": echo "Tag: Stei1e[day]<br>"; echo "Monat: Stei1e[month]<br>"; echo "Jahr: Stei1e[year]<br>"; Listing 8.3 Einlesen eines Datums Die einzelnen Bestandteile des Datums werden nebst einiger anderer Informati- onen im Array Steile zurückgegeben. Die Methode getDateFormat() liest die Formatierung aus, die im Locale-Objekt enthalten ist. Wie bereits gesagt, sind in den Locale-Objekten nur die »üblichen« Datumsfor- mate enthalten. Möchten Sie beispielsweise ein Datum verarbeiten, das nach DIN aufgebaut ist, so wäre das nicht ganz so einfach. Ein Datum nach DIN enthält Jahr, Monat und Tag. In dem Fall könnten Sie dem Array direkt die Formatinfor- mation übergeben. 378
e-bol.net Lokalisierung mit Zend_Locale | 8.1 http://...ng/6.php CD Datumsformat: dd.MM.yyyy Datum: 10.03.1970 Tag: 10 Monat: 03 Jahn 1970 Abbildung 8. 4 Ausgabe des analysierten Datums Dazu können Sie dem Schlüssel date_format den String ' yyyy-MM-dd' mitgeben: Sdatum = '1970-10-03’; $format - ’yyyy-MM-dd': Steile = Zend_Locale_Format::getDate($datum, array(’date_format’=>$format)): Sie benötigen in diesem Zusammenhang auch kein Locale-Objekt mehr, da Sie die Formatinformationen ja auf anderem Weg bereitgestellt haben. Innerhalb des Format-Strings können Sie das y als Platzhalter für das Jahr nutzen, das M steht für den Monat und das d für den Tag. Erwarten Sie ein vierstelliges Jahr, sollten sich auch vier y in dem String finden, was dementsprechend auch für den Monat und den Tag gilt. Sollte die Methode darüber stolpern, dass ein Datum beispielsweise die 13 als Monat enthält, wirft sie eine Exception. Hier gibt es allerdings ein interessantes »Hintertürchen«. Und zwar können Sie im Array mit den Optionen den Schlüssel ’ f 1 x_date' mit dem Wert true belegen. In dem Fall versucht die Methode, den übergeben Wert zu korrigieren und tauscht den Tag mit dem Monat, falls der angegebene Tag kleiner oder gleich 12 ist. Um Ihnen mitzuteilen, was die Methode verändert hat, liefert Sie im Ergebnis-Array einen weiteren Wert im Feld ’ f i xed ' zurück. Die Bedeutung der Werte finden Sie in Tabelle 8.3. Wert Bedeutung 0 keine Änderung 1 Monat korrigiert 2 Tag und Jahr getauscht 3 Monat und Jahr getauscht 4 Monat und Tag getauscht Tabelle 8.2 Rückgabewerte bei einem korrigierten Datum 379
e-bol.net 8 | Lokalisierung und Internationalisierung Alternativ können Sie das Datum vorher auch an checkDate() übergeben. Diese Methode akzeptiert die gleichen Parameter und liefert true oder fal se zurück, um Ihnen mitzuteilen, ob ein Datum verarbeitet werden kann. Um eine Uhrzeit zu verarbeiten, stehen ähnliche Methoden zur Verfügung. Mit getTimel) zerlegen Sie eine Uhrzeit in ihre Bestandteile. Die Methode bekommt an erster Stelle die Uhrzeit als String übergeben und akzeptiert als zweiten Para- meter wieder ein Array. Mit Ausnahme des Schlüssels ’fix_date' können Sie hier dieselben Bestandteile nutzen. Um ein Format zu beschreiben, stehen hier andere Platzhalter zur Verfügung. Das h wird als Platzhalter für die Stunden genutzt, m steht für die Minuten und mit s werden die Sekunden symbolisiert: requi re_once ’Zend/Locale/Format.php'; $zeit = '23 11 523* ; Sformat = 'hh mm ss'; Steile = Zend_Locale_Format::getTime($zeit, array(’date_format'=>$format)); echo "<br>Zeit: $zeit<br>”; echo "Stunden: $tei1e[hour]<br>"; echo "Minuten: $tei1e[minute]<br>"; echo "Sekunden: $tei1e[second]<br>"; Listing 8.4 Einlesen einer Uhrzeit Die einzelnen Bestandteile der Uhrzeit können Sie einem Array entnehmen, das die Schlüssel hour, minute und second hat. Die hohe Anzahl der Sekunden ist übrigens kein Tippfehler, sondern sollte nur zeigen, dass die Methode auch mit Zahlen umgehen kann, die größer sind als das, was in einer gewöhnlichen Uhr- zeitangabe zulässig ist. Die Obergrenze ist hier nur durch PHP vorgegeben. Noch nicht erwähnt hatte ich, dass die Methode getDatei) auch in der Lage ist, mit Uhrzeiten umzugehen. Sollten Sie also Daten haben, in denen Datum und Uhrzeit enthalten sind, dann kann getDatei) diese Informationen auch verarbei- ten, sofern Sie der Methode einen korrekten Format-String übergeben, wobei dieser dann auch h, m und s enthalten darf. Um zu prüfen, ob eine Uhrzeit das korrekte Format hat, können Sie auch die Methode checkDatel) verwenden. 380
e-bol.net Mehrsprachige Oberflächen mit Zend_Translate | 8.2 8.2 Mehrsprachige Oberflächen mit Zend_Translate Um eine Applikation auf internationaler Ebene zu nutzen, sollte sie natürlich mehrsprachig sein. Dazu bietet sich die Nutzung von Zend_Transl ate an. Die Idee dahinter ist einfach. Sie lassen mithilfe einer Methode einen Text in einer »Standardsprache« ausgeben. Die Methode prüft, in welcher Sprache die Appli- kation ausgeführt wird, und gibt den Text in der entsprechenden Sprache aus, sofern eine Übersetzung vorliegt. Der Text in der Standardsprache dient dabei als eine Art Schlüssel in einem Array. Für jede Sprache, in der die Applikation genutzt werden soll, muss eine solche Übersetzungstabelle vorliegen. Das heißt, wenn Sie das Wort »Auto« auf Englisch ausgeben wollten, würde Zend_Trans- 1 ate in der Tabelle prüfen, wo der Text »Auto« zu finden ist, und dann die dazu- gehörige Übersetzung ausgegeben. Diese Idee einer »Übersetzungstabelle« ist in verschiedenen Formen implemen- tiert. Zwar werden auch die oben erwähnten Arrays unterstützt, aber Sie können auch auf andere Datenquellen zurückgreifen. In Tabelle 8.4 finden Sie eine Liste der momentan unterstützten Datenquellen. Adapter Konstante Beschreibung Array AN-ARRAY Ein normales PHP-Array, das in den Quelltext eingebettet ist. Es kann sich dabei auch um eine inkludierte Datei handeln. Csv AN-CSV Übersetzung auf Basis einer Textdatei, bei der die einzelnen Werte durch ein Trennzeichen (Komma, Semikolon o.Ä.) getrennt sind. Gettext AN.GETTEXT Basiert auf einer .mo-Datei. Diese muss kompiliert werden. Aufgrund der Performance ist dies ideal für große Anwendungen. TMX AN.TMX auf XAAL basierendes Dateiformat QT AN_QT auf XAAL basierendes Dateiformat XLIFF AN-XLIFF auf XAAL basierendes Dateiformat TBX AN.TBX auf XAAL basierendes Dateiformat XMLTM AN.XMLTM auf XAAL basierendes Dateiformat Tabelle 8.3 Adapter für Zend_Translate Im Folgenden werde ich auf die Arbeit mit Arrays und CSV-Dateien eingehen, da diese in der Lage sind, »Übersetzungstabellen« mit einem recht geringen Auf- wand zu erstellen. Die Nutzung von CSV können Sie aber unproblematisch auf alle anderen Dateiformate übertragen. Sollten Sie eine größere Website lokalisieren wollen, so empfiehlt es sich aller- dings, mit der Gettext-Variante zu arbeiten, weil diese am performantesten ist. Da die .mo-Dateien allerdings plattformabhängig erstellt werden müssen, ist es hier 381
e-bol.net 8 | Lokalisierung und Internationalisierung schwierig, dies für jedes Betriebssystem zu erläutern. Eine Einführung in die Erstellung von .mo-Dateien finden Sie unter http://www.gnu.org/software/gettext oder in der Dokumentation des Zend Frameworks. Dort sind auch noch weiter- gehende Informationen zu den verschiedenen Datenquellen vorhanden. Ebenso finden Sie dort auch Informationen, wie Sie selbst Adapter erstellen und nutzen. Um mit Zend_Translate arbeiten zu können, benötigen Sie ein Objekt der Klasse. Wenn Sie das Objekt ableiten, müssen Sie dem Konstruktor mitteilen, welche Art von Übersetzungsinformationen vorliegt. Das geschieht dadurch, dass Sie die Konstante, die Sie neben dem Namen des Adapters (Tabelle 8.3) finden, als ersten Parameter angeben. Die Konstanten sind alle in der Klasse Zend_Trans- late deklariert. Der zweite Parameter ist dann die Information, welche Datei oder welches Array zu nutzen ist. An dritter Stelle können Sie optional die Infor- mation übergeben, welche Sprache in der Datei bzw. in dem Array enthalten ist. So lange nur eine Sprache genutzt wird, ist diese Information überflüssig. Möch- ten Sie diesen Parameter nutzen, können Sie ihn in Form eines Strings oder eines Zend_Locale-Objekts übergeben. Möchten Sie die Lokalisierung auf Basis von Arrays vornehmen, könnte das so aussehen: requi re_once ’Zend/Translate.php'; Sdeutsch = array ('next' => 'vor', 'back' => ’zurück', 'finish* => 'beenden' ): Stranslate = new Zend_Translate (Zend_Locale::AN_ARRAY, tdeutsch); // Gibt 'zurück' aus echo $translate->_('back’); // Gibt 'vor' aus echo $translate->_('next'): // Gibt ’commit' aus echo $translate->_('commit'); Listing 8.5 Nutzung von Zend_Translate In dem Array finden sich die englischen Wörter als Schlüssel und die deutsch- sprachigen Begriffe als dazugehörige Übersetzung. Nachdem ein entsprechendes Objekt abgleitet wurde, können die jeweiligen Übersetzungen mithilfe der Methode _() ausgelesen werden. Diese bekommt das gewünschte Wort als Para- 382
e-bol.net Mehrsprachige Oberflächen mit Zend_Translate | 8.2 meter übergeben und gibt die Übersetzung zurück. In der letzten Zeile soll das Wort commi t in der lokalisierten Variante ausgegeben werden. Wie Sie sehen, ist das allerdings nicht in dem Array vorhanden. In so einem Fall wird das ursprüng- liche Wort, also commi t, ausgegeben. Daher empfiehlt es sich, Englisch als »Mas- tersprache« zu nutzen. Sollte ein Wort mal nicht übersetzt sein, so kommen die meisten Menschen sicher auch mit der englischen Variante zurecht. Diese Vorgehensweise bietet sich an, wenn der Benutzer die Sprache manuell einstellen kann. Allerdings teilt der Browser dem Server ja schon mit, welche Sprachen bevorzugt werden. Ist der Browser korrekt konfiguriert, so können Sie diese Information direkt übernehmen. Das setzt allerdings voraus, dass Sie meh- rere Sprachen beim System anmelden, aus denen dann selektiert werden kann. Um mehrere Sprachen zu registrieren, steht die Methode addTranslationf) zur Verfügung. Mit ihr können Sie eine zusätzliche Sprache beim System anmelden: requi re_once ’Zend/Translate.php'; require_once 'Zend/Locale.php' ; Sdeutsch = array ('next' => 'vor', 'back' => ’zurück', ’finish’ => 'beenden' $franzoesisch = array ('next' => 'avant', 'back’ => 'retour’, ’finish' => ’finir' $locale_de = new Zend_Locale(’de_DE'); $locale_fr = new Zend_Locale('fr_FR'); Stranslate = new Zend_Translate (Zend_Translate::AN_ARRAY, Sdeutsch, $locale_de): Stranslate->addTranslation($franzoesisch, $1ocale_fr); // Gibt 'retour' aus echo $translate->_('back’, $locale_fr); // Gibt 'vor' aus echo $translate->_('next', ’de_DE’); Listing 8.6 Lokalisierung mit Zend_Translate In diesem Beispiel wurden die beiden Sprachen mithilfe von Locale-Objekten angemeldet. Genau so hätte allerdings auch ein String wie 'de' oder 'de_DE' genutzt werden können. Das Gleiche gilt für den Zugriff auf die Übersetzungen. 383
e-bol.net 8 | Lokalisierung und Internationalisierung Da zwei Sprachen bekannt sind, sollten Sie der Methode _() mitteilen, welche der Sprachen genutzt werden soll. Dazu können Sie als zweiten Parameter entwe- der einen String oder ein Locale-Objekt übergeben. Um das nicht bei jedem Auf- ruf machen zu müssen, können Sie die Lokalisierungsinformation auch einmalig mit der Methode setLocale() setzen. Die Methoden lesen diese Daten dann jeweils aus. Sie sehen, die Nutzung dieser Klasse ist erfreulich einfach und unpro- blematisch. Die Klasse stellt auch noch einige recht hilfreiche Methoden zur Verfügung, um die Übersetzungen zu verwalten. Mit getMessageldsl) können Sie alle Wörter in der Mastersprache auslesen, die dem Adapter bekannt sind. Bezogen auf das letzte Beispiel würden Sie hier ein Array erhalten, in dem die Strings next, back und finish enthalten sind. Optional können Sie der Methode noch eine Lokali- sierungsinformation in Form eines Strings oder eines Locale-Objekts übergeben, um herauszufinden, welche Schlüssel in welchen Sprachen vorhanden sind. Wollen Sie testen, ob eine bestimmte Sprache beim aktuellen Adapter angemel- det ist, so übergeben Sie die fragliche Sprachinformation an die Methode i s - Avai 1 abl e(), die Ihnen einen booleschen Wert zurückgibt. Ob eine einzelne Übersetzung vorhanden ist, können Sie mit der Methode i sTransl ated() überprüfen, welche den String übergeben bekommt, der über- setzt werden soll. 8.2.1 Nutzung von CSV-Dateien CSV-Dateien sind für die Erstellung mehrsprachiger Anwendungen oft sehr prak- tisch. Zwar ist ihre Nutzung nicht sehr performant, aber sie haben den großen Vorteil, dass sie mit Excel erstellt werden können. Sie können den Übersetzern einfach eine Excel-Tabelle schicken, in der eine Spalte mit den Begriffen in der Mastersprache enthalten ist. Die Übersetzer notieren dann einfach den übersetz- ten Begriff daneben und exportieren die Datei im CSV-Format, wobei sie idealer- weise den UTF-8-Zeichensatz nutzen sollten. Eine solche Datei kann dann bei- spielsweise so aussehen: next:vor back;zurück finish;beenden Jede Übersetzung ist in einer eigenen Zeile zu finden. Werden diese Daten in einer Datei namens de.csv gespeichert, kann die Nutzung so aussehen: 384
e-bol.net Konvertieren von und Rechnen mit Maßeinheiten mittels Zend_Measure | 8.3 requi re_once ’Zend/Translate.php'; require_once 'Zend/Locale.php'; $locale_de = new Zend_Locale(’de_DE'): Stranslate = new Zend_Translate (Zend_Translate::AN_CSV, 'de.csv'. $locale_de); Stranslate->setLocale($1ocale_de); // Gibt 'vor' aus echo $translate->_('next'); Listing 8.7 Nutzung einer CSV-Datei Anstelle des Arrays bekommt der Konstruktor einfach nur den Dateinamen inklusive des Pfads übergeben. In den meisten CSV-Dateien wird das Semikolon als Trennzeichen zwischen den Spalten genutzt. Sollten Sie allerdings ein anderes Zeichen nutzen, ist das kein großes Problem. In dem Fall übergeben Sie dem Konstruktor nach der Lokalisierungsinformation noch ein Array, das den Schlüs- sel Separator enthält. Der Wert dieses Schlüssels ist dann das genutzte Trennzei- chen. Mit dieser Instantiierung Stranslate = new Zend_Translate (Zend_Translate::AN_CSV, 'de.csv*, $locale_de, array(’Separator' => wird die Datei de.csv eingebunden, wobei das Komma als Trennzeichen erwartet wird. Wie gesagt, können Sie die anderen Dateiformate auf die gleiche Art ein- binden. Bei den anderen Formaten steht der Schlüssel separater allerdings nicht zur Verfügung, da er nicht benötigt wird. Ab der Version 1.1 des Zend Frameworks wird die Möglichkeit zur Verfügung stehen, dass Sie nur ein Unterverzeichnis angeben, und Zend_Translate dann selbstständig nach dort enthaltenen Dateien sucht. Da dieses Feature momentan noch nicht implementiert ist, kann ich Sie an dieser Stelle nur auf das Manual verweisen. Bitte beachten Sie, dass das Scannen der Unterverzeichnisse unter Umständen recht zeitintensiv sein kann. Daher sollten Sie darüber nachdenken, die Dateinamen dynamisch zu konstruieren und auf die oben beschriebene Weise zu inkludieren. 8.3 Konvertieren von und Rechnen mit Maßeinheiten mittels Zend_Measure In verschiedenen Kulturkreisen sind nicht nur unterschiedliche Sprachen oder Kalendersysteme, sondern auch unterschiedliche Maßsysteme anzutreffen. Am bekanntesten hierbei sind sicher das metrische System (auch Sl-System genannt), 385
e-bol.net 8 | Lokalisierung und Internationalisierung das im kontinentalen Europa verwendet wird, und das Zoll-System, welches noch im Vereinigten Königreich und in den USA Verwendung findet. Die Konvertierung zwischen den einzelnen Maßeinheiten ist nicht immer ganz einfach. Zum Ersten müssen Sie natürlich wissen, welche Maßeinheiten in der jeweiligen Region genutzt werden. Der zweite Punkt ist, dass Sie die Umrech- nungsfaktoren kennen müssen. So ist beispielsweise ein deutscher Zoll 26,1 mm lang, wohingegen ein Zoll in England 25,4 mm misst. Im Gegensatz zu anderen Klassen gibt es in diesem Fall keine Datei namens Measure.php, die Sie inkludieren könnten. Hier müssen Sie die benötigten Dateien jeweils für eine »Gattung« von Maßeinheiten inkludieren. Das heißt, es gibt eine Datei, die für Längeneinheiten zuständig ist, eine für Gewichte, eine für Flächen usw. Das ist auch sinnvoll, da Sie die Werte natürlich nicht von Meter nach Grad Celsius oder von Hektar nach Inch umrechnen können. Eine Längen- einheit kann also nur in eine andere Längeneinheit konvertiert werden etc. Zurzeit sind 28 Klassen für die verschiedenen Maßeinheiten definiert. Jede Klasse verfügt über eine eigene Klassen-Datei im Ordner Zend/Measure. Für die Arbeit mit Frequenzen findet sich hier beispielsweise die Datei Frequency.php, in der die Klasse Zend_Measure_Frequency deklariert ist. In drei Fällen (Kochen, Fließen und Viskosität) sind die Klassen noch einmal in eigenen Unterverzeich- nissen abgelegt. Um die Mengenangabe »ein Teelöffel« in Gramm umzurechnen, könnten Sie also Zend_Measure_Cooking_Weight nutzen. Welche Klassen insge- samt vorhanden sind, entnehmen Sie bitte dem Manual unter http://framework. zend.com/manual/de/zend.measure.types.html. Sämtliche Klassen erweitern eine abstrakte Klasse, sodass die Nutzung in allen Fällen identisch ist. In den Klassen sind jeweils Konstanten für die verschiedenen Maßeinheiten deklariert. Bei der Nutzung ist es natürlich hilfreich, die Maßein- heiten zu kennen. Dazu können Sie in den Quelltext der Klasse schauen, oder Sie nutzen diese Zeilen, um sich die deklarierten Konstanten ausgeben zu lassen: require_once 'Zend/Measure/Cooking/Weight.php'; $ref = new ReflectionClass('Zend_Measure_Cooking_Weight'): echo "<b>Deklarierte Konstanten:</b><br>”; foreach ($ref->getConstants() as $konst) { echo "$konst<br>"; 1 Natürlich müssten Sie immer den Namen der Klasse einsetzen, welche Sie unter- suchen wollen. 386
e-bol.net Konvertieren von und Rechnen mit Maßeinheiten mittels Zend_Measure | 8.3 Die Nutzung ist einfach. Zunächst leiten Sie ein Objekt der entsprechenden Klasse ab. Dem Konstruktor übergeben Sie als ersten Parameter die Information, um »wie viel« es sich handelt. Sie geben also einfach eine Zahl an. An zweiter Stelle können Sie die Maßeinheit in Form einer Konstante angeben. Tun Sie dies nicht, wird eine Standardmaßeinheit zugrunde gelegt. Als dritten Parameter kön- nen Sie optional noch ein Zend_Local e-Objekt übergeben. Daraus ermittelt die Klasse, wie Zahlen formatiert sein können bzw. zu formatieren sind. Was dies konkret bedeutet, sehen Sie im folgenden Beispiel: requi re_once 'Zend/Measure/Wei ght.php'; // "Zahl" die eingelesen werden soll $zahl = ”1.234,56"; // Englische Lokalisierung Slocale = new Zend_Locale('en'); Smeasure = new Zend_Measure_Weight( $zahl,Zend_Measure_Weight::GRAM, $ locale); echo $measure->getValue(); echo "<br>"; // Deutsche Lokalisierung Slocale = new Zend_Locale(* de'); Smeasure = new Zend_Measure_Weight( $zahl,Zend_Measure_Weight::GRAM, $ locale); echo $measure->getValue(); Listing 8.8 Nutzung von Zend_Measure In diesem Beispiel wurde die Zahl in Form eines lokalisierten Strings an den Kon- struktor übergeben. Abhängig von der Lokalisierung, die der Klasse mitgeteilt wird, haben Punkte und Kommas eine andere Bedeutung, was dazu führt, dass im ersten Fall 1.23 ausgegeben wird, wohingegen im zweiten Fall 1234.56 auf dem Bildschirm erscheint. In zukünftigen Versionen der Klasse soll diese Lokali- sierung auch Auswirkung auf die Formatierung der Zahlen bei der Ausgabe haben, was aber noch nicht der Fall ist. Sie können den Wert eines Objekts auch nachträglich durch Aufruf der Methode setValue() ändern, die die gleichen Parameter akzeptiert wie der Konstruktor. Wie Sie dem Beispiel schon entnehmen konnten, können Sie mit der Methode getVal ue() den Wert auslesen, der in einem Maßeinheiten-Objekt enthalten ist. Die dazugehörige Maßeinheit stellt die Methode getType() zur Verfügung. Die Maßeinheiten werden immer als englischsprachige Strings in Großbuchstaben zurückgegeben, sodass Sie sich selbst um eine Lokalisierung kümmern müssen. 387
e-bol.net 8 | Lokalisierung und Internationalisierung Es wäre natürlich nicht sonderlich spannend, einen Wert zu übergeben und ihn gleich wieder auszulesen. Möchten Sie einen Wert in eine andere Maßeinheit konvertieren, so nutzen Sie die Methode convertTol), welche als Parameter eine Konstante übergeben bekommt, die definiert, in welche Einheit konvertiert wer- den soll. Die Methode liefert den konvertierten Wert direkt als String mit ange- hängter Maßeinheiten-Abkürzung zurück, sodass sie ihn direkt ausgeben kön- nen. Darüber hinaus wird aber auch die Information innerhalb des Objekts geändert, sodass Sie getVal ue() und getType() nutzen können: require_once 'Zend/Measure/Cooking/Weight.php'; Slocale = new Zend_Locale(’de'): Smeasure = new Zend_Measure_Cooking_Weight("2,5", Zend_Measure_Cooking_Weight::TEASPOON, $ 1ocale); echo "Werte vor der Konvertierung: echo $measure->getValue(). ” ”: echo $measure->getType(): echo "<br>"; echo "Direkte Ausgabe: echo $measure->convertTo(Zend_Measure_Cooking_Weight::GRAM); echo "<br>"; echo "Manuelle Ausgbe: echo $measure->getValue()." echo $measure->getType(); Die Ausgabe dieses Beispiels, bei dem 2,5 Teelöffel in Gramm umgerechnet wer- den, finden Sie in Abbildung 8.5. O http://carsten-.,.alisierung/8.php CD Werte vor der Konvertierung: 2.5 TEASPOON Direkte Ausgabe: 8.86 g Manuelle Ausgbe: 8.86 GRAM Abbildung 8.5 Ausgabe der Werte Ein String, den convertTo(), liefert können Sie auch jederzeit durch Aufruf der Methode toString() generieren. Den Methoden getVal ue(), convertTol) und toStri ng() können Sie optional noch die Anzahl der Stellen übergeben, auf wel- 388
e-bol.net Währungsdarstellung mit Zend_Currency | 8.4 ehe die Zahl gerundet werden soll. toStringO rundet standardmäßig nicht, wohingegen die beiden anderen per Default nur zwei Nachkommastellen liefern. Neben der reinen Konvertierung kann die Klasse aber noch mehr. Sie können auch mit Werten rechnen und diese vergleichen. Injedem Zend_Measure-Objekt stehen die Methoden add () und sub() zur Verfügung, welche ein Objekt der gleichen Klasse als Parameter übergeben bekommen. Der Wert des übergebenen Objekts wird dann entweder addiert oder subtrahiert und der neue Wert im ursprünglichen Objekt gespeichert, wobei das Ergebnis der Berechnung von den Methoden direkt zurückgegeben wird. Bitte beachten Sie, dass beide Methoden intern mit Werten rechnen, die auf die zweite Nachkommastelle gerundet sind. $ml = new Zend_Measure_Weight(1.1 , Zend_Measure_Wei ght::CATTY_THAI); $m2 = new Zend_Measure_Weight(1.235 , Zend_Measure_Wei ght::CATTY_THAI); echo $ml->add($m2); // Ausgabe: 2.34 catty Um zwei Objekte zu vergleichen, stehen die Methoden equal s() und compare() zur Verfügung. Die erste liefert true zurück, wenn das aktuelle Objekt sowie das- jenige, das als Parameter übergeben wird, bis zur letzten Kommastelle identisch sind. Die Methode compare() gibt -1 zurück, wenn das Objekt, aus dem heraus die Methode aufgerufen wurde, einen kleineren Wert hat als das übergebene Objekt. Sie gibt 1 zurück, wenn der Wert des übergebenen Objekts größer ist, und 0, falls beide gleiche Werte haben. Diese Methode können Sie beispielsweise als Callback für Sortierfunktionen nutzen. Bitte beachten Sie, dass die Einheit des übergebenen Objekts nach dem Aufruf der Einheit des anderen Objekts ent- spricht. 8.4 Währungsdarstellung mit Zend_Currency Die Klasse Zend_Currency ist eine sehr praktische Klasse, wenn es darum geht, Währungsangaben zu formatieren. Zend_Currency dient allerdings wirklich nur der Darstellung und bietet keine Möglichkeit, von einer Währung in eine andere umzurechnen. Bei der Darstellung von Währungen sind drei Punkte zu beachten. Der erste Punkt ist die Währung, in der die Summe dargestellt wird. Hier gilt es, die kor- rekte Abkürzung bzw. das korrekte Währungssymbol zu wählen. Zum Zweiten gilt es, die korrekten Dezimal- und Tausender-Trennzeichen zu wählen. Der dritte Punkt betrifft die Darstellung der Zahlen - mit normalen arabischen Ziffern 389
e-bol.net 8 | Lokalisierung und Internationalisierung oder in einer anderen Notation. Zwar ist das rein technisch nicht so schwierig, aber die dahinterliegenden Informationen zusammenzutragen, ist ein nicht zu unterschätzender Aufwand. Bei der Arbeit mit Zend_Currency stellt sich zunächst die Frage, welche Währun- gen dem System bekannt sind. Zend_Currency greift im Hintergrund auf die Informationen von Zend_Locale zurück, sodass der volle Informationsumfang der Klasse hier zur Verfügung steht, und Sie somit davon ausgehen können, dass alle relevanten Länder und Währungen hinterlegt sind. Im einfachsten Fall könnte die Nutzung so aussehen: header('Content-Type: text/html; charset=utf-8'): requi re_once 'Zend/Currency.php’; Scurrency = new Zend_Currency(1 EUR', 'de_DE*): echo $currency->toCurrency( 10000): // Ausgabe: 10.000,00 € Listing 8.9 Nutzung von Zend_Currency In diesem Beispiel wurden dem Konstruktor zwei Informationen übergeben. Bei dem ersten Parameter handelt es sich um die Währung, die genutzt werden soll. Er definiert, entsprechend welcher Lokalisierung die Zahl formatiert werden soll. Die Währung muss immer in der offiziellen Abkürzung mit drei Buchstaben übergeben werden: EUR definiert also den Euro, USD steht für US-Dollar und GBP kürzt britische Pfund ab. Eine Liste aller Abkürzungen finden Sie beispiels- weise unter http://air-ways.de/waehrungen.htm. Bei dem zweiten Parameter handelt es sich um einen ganz normalen Locale- String, der Sprache und Lokalisierung definiert. Er legt fest, wie die Tausender- und Dezimal-Trennzeichen aussehen sollen. Sie können an dieser Stelle auch ein Zend_Locale-Objekt mit der entsprechenden Lokalisierung übergeben. Der Konstruktor ist übrigens sehr flexibel. Sie müssen ihm nicht unbedingt beide Parameter übergeben. Aufgrund seiner Implementation erkennt er durchaus auch, wenn Sie ihm nur den Lokalisierungs-String, also zum Beispiel de_DE oder ein Zend_Local e-Objekt übergeben. In dem Fall werden die anderen Informatio- nen entsprechend ergänzt, sodass als Währung Euro selektiert wird. Der Aufruf der Methode toCurrency() wandelt die übergebene Zahl in eine kor- rekt formatierte Währungsangabe um und gibt diese zurück. Hierbei ist zu beach- ten, dass die Währung standardmäßig als Symbol zurückgegeben wird. Die Sym- bole werden dabei grundsätzlich in UTF-8-Codierung zurückgegeben, um 390
e-bol.net Währungsdarstellung mit Zend_Currency | 8.4 Zeichensatzprobleme zu vermeiden. Wichtig ist auch, dass die Methode nicht immer mit zwei Nachkommastellen arbeitet. Daraus resultiert im obigen Beispiel auch, dass 10.000,00 € und nicht 10.000 € ausgegeben wird. Die Informationen, die Sie dem Konstruktor übergeben haben oder die der Kon- struktor selbst ermittelt hat, können Sie natürlich auch noch nachträglich verän- dern. So können Sie der Methode toCurrency() an zweiter Stelle ein Array mit Informationen übergeben, die für die Formatierung der Zahl genutzt werden. Alternativ haben Sie auch die Möglichkeit, die Daten mithilfe der Methode set- Format() festzulegen. In dem Fall gelten sie nicht nur für die eine Ausgabe, son- dern für alle Konvertierungen, die danach noch erfolgen. Dieses Array darf bestimmte Schlüssel enthalten. Da ist zum zunächst der Schlüs- sel di splay. Damit definieren Sie, wie die Währung dargestellt werden soll, ob als Symbol, Abkürzung oder als ausgeschriebener Text. Diesen Schlüssel können Sie mit den Konstanten aus Tabelle 8.4 belegen. Konstante Bedeutung NO_$YMBOL keine Währungsinformation ausgeben USE_SYMBOL Währungssymbol ($, €usw.) nutzen USE_SHORTNAME Abkürzung der Währung (EUR, USD usw.) ausgeben USE_NAME Name der Währung (American Dollar) Tabelle 8.4 Konstanten für die Währungsausgabe In diesem Zusammenhang sind auch die Schlüssel name, currency und Symbol interessant. Möchten Sie nicht die vorgegebenen Währungsnamen bzw. Symbole nutzen, so können Sie mit diesen Schlüsseln andere übergeben. Mit name können Sie den Namen einer Währung übergeben, wie beispielsweise Euro. Der hier ent- haltene Name wird dargestellt, wenn Sie mit di spl ay USE_NAME übergeben. Soll- ten Sie di spl ay mit USE_SHORTNAME belegt haben, wird die Währungsabkürzung dargestellt, die Sie in currency übergeben haben. Um ein eigenes Symbol an Stelle des Euro-Zeichens oder eines anderen Symbols zu nutzen, weisen Sie das Symbol zu und übergeben USE_SYMBOL mithilfe von di spl ay. Der nächste zur Verfügung stehende Schlüssel ist posi ti on. Mit ihm wird dekla- riert, wo die Ausgabe des Währungssymbols bzw. -namens erfolgen soll. Hierbei können Sie die Konstanten aus Tabelle 8.5 nutzen. 391
e-bol.net 8 | Lokalisierung und Internationalisierung Konstante Bedeutung STANDARD Die Position der Währungsangabe entsprechend der Lokalisierung RIGHT Die Währungsangabe erscheint rechts neben der Zahl. LEFT Die Währungsangabe erscheint links neben der Zahl. Tabelle 8.5 Konstanten zur Positionierung der Währung Wie viele Nachkommastellen die Ausgabe haben soll, definieren Sie über das Ele- ment precision, dem Sie eine Zahl übergeben, die die Anzahl der Stellen nach dem Komma festlegt. Sollten Sie eine andere Lokalisierung nutzen wollen, so haben Sie die Möglichkeit, format ein Locale-Objekt oder einen -String zuzuwei- sen. Mit der letzten Option script haben Sie die Möglichkeit, ostarabische Zif- fern zu nutzen, indem Sie ihr den String Arab zuweisen. Die westarabischen Zif- fern selektieren Sie mit Latn. Das System kennt noch eine ganze Reihe anderer Darstellungsarten. Eine komplette Liste finden Sie in der Dokumentation zu Zend_Local e.4 Die folgenden Zeilen enthalten einige Beispiele, deren Ausgabe Sie in Abbildung 8.6 sehen. header('Content-Type: text/html; charset=utf-8'); requi re_once 'Zend/Currency.php’; Slocale = new Zend_Locale(’de_DE'); Scurrency = new Zend_Currency($1ocale); $opts = array ('position' => Zend_Currency::LEFT, 'precision' => 3, 'name’ => 'Euronen: ', 'display' => Zend_Currency::USE_NAME ); echo 'Drei Nachkommastellen, Währung "Euronen" links:<br>'; echo $currency->toCurrency( 10000, $opts).'<br>'; $opts = array ('position' => Zend_Currency::RIGHT, 'script' => 'Latn', 'precision' => 2, 'display' => Zend_Currency::USE_SHORTNAME ); echo 'Zwei Nachkommastellen, Währung "EUR" rechts:<br>'; echo $currency->toCurrency(123.456, $opts).’<br>'; $opts = array ('precision' => 2, 4 http://framework.zend.eom/manual/en/zend.locale.parsing.html# zend. locale, appendix.numberscripts.supported 392
e-bol.net Währungsdarstellung mit Zend_Currency | 8.4 ’display' => Zend_Currency::USE_SYMBOL, ’format' => ’enJJS' ): echo 'Zwei Nachkommastellen, Währungssymbol, amerikanische Formatierung:<br>': echo $currency->toCurrency(1234.567, $opts),'<br>' ; $opts = array (’script' => ’Arab'); echo 'Ostarabische Ziffern, zwei Nachkommastellen, Währungssymbol: <br>*; echo $currency->toCurrency(1234.567, $opts).'<br>'; Listing 8.10 Ausgabe verschieden formatierter Währungen C« M http://12 7.0.0. l/~carsten/zf-buch/Currency/l.php CD ä w uö "© I’ Q’ Google [u j Drei Nachkommastcllcn, Währung "Euroncn" links: Euroncn: 10.000,000 Zwei Nachkommastcllcn, Währung "EUR" rechts: 123,46 EUR Zwei Nachkommastcllcn, Währungssymbol, amerikanische Formatierung: 1,234.57 € Ostarabischc Ziffern, zwei Nachkommastcllcn, Währungssymbol: x.vrt.ov € Abbildung 8.6 Ausgabe mit verschiedenen Formatierungen In der Klasse sind noch einige zusätzliche Funktionen deklariert, welche durch- aus hilfreich sein können, um eigene Ausgaben zu erstellen. Den Namen der Währung können Sie in der Kurzform mit der Methode getShortName() ausge- ben lassen, die ausgeschriebene Variante erhalten Sie mit getNameO, und getSymbol () liefert Ihnen das Währungssymbol im UTF-8 Zeichensatz zurück. Die drei Methoden können ohne Parameter direkt aus einem Zend_Currency- Objekt heraus aufgerufen werden und liefern dann die entsprechenden Informa- tionen für die aktuelle Lokalisierung. Alternativ können Sie ihnen auch eine Währung und/oder ein Locale-Objekt bzw. einen Locale-String übergeben: header('Content-Type: text/html; charset=utf-8'): requi re_once 'Zend/Currency.php’; Slocale = new Zend_Locale('de_DE'): Scurrency = new Zend_Currency($locale): 393
e-bol.net 8 | Lokalisierung und Internationalisierung echo $currency->getName()."<br>": //Euro echo $currency->getShortName(). "<br>"; //EUR echo $currency->getSymbol()."<br>": //€ echo $currency->getName(’en_US')."<br>"; // US Dollar echo $currency->getShortName('en_US')."<br>"; //USD echo $currency->getSymbol('en_US')."<br>"; Listing 8.11 Ausgabe einzelner Währungsbestandteile Interessant sind auch noch die Methoden getRegionList() und getCurrency- Li st(). Die erste bekommt eine Währung übergeben und liefert Ihnen ein Array mit den Codes aller Länder, welche die Währung einsetzen: echo "Der Euro wird genutzt in: echo implodet’, ’,$currency->getRegionList(’EUR’)): /* Ausgabe: Der Euro wird genutzt in: AD, AT, AX, BE, CS, CY, DE, ES, FI, FR, GF , GP. GR, IE, IT, LU. MC, ME. MQ, MT, NL, PM, PT, RE, SI. SM, TF. VA . YT */ Die zweite bekommt den Code eines Landes übergeben und gibt Ihnen ein Array mit den dort genutzten Währungen: $erg = $currency->getCurrencyList('DE'); pri nt_r($erg); /* Ausgabe: Array ( [EUR] => 1999-01-01 [DEM] => 1948-06-20 ) */ Wie Sie sehen, wird hier der Kurzname der Währung als Schlüssel genutzt. Der dazugehörige Wert ist das Datum, zu dem die Währung eingeführt wurde. Eine kleine Anmerkung zum Abschluss: Aufgrund der großen Datenmenge, die Zend_Currency analysieren muss, dauert es oft ein wenig, bis Sie die Daten erhal- ten. Daher haben die Entwickler von vornherein die Möglichkeit vorgesehen, die Daten zu cachen. Dazu können Sie ein Objekt der Klasse Zend_Cache ableiten und es mit der statischen Methode setCache() an die Klasse Zend_Currency überge- ben. Sollte Ihre Applikation öfter Zend_Currency nutzen, sollten Sie einen Cache verwenden. 394
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 8.5 Datums- und Zeitangaben mit Zend_Date verarbeiten Die Klasse Zend_Date stellt Ihnen sehr umfangreiche Möglichkeiten zur Arbeit mit Zeit- und Datumsangaben zur Verfügung. Besonders angenehm bei der Nut- zung der Klasse sind das hohe Maß an Flexibilität und die große Anzahl der Methoden. Zugegebenermaßen ist die große Anzahl der Funktionen auch ein wenig verwirrend, aufgrund der klaren Struktur kann man aber gut damit leben. Zend_Date kann übrigens mit beliebigen Daten rechnen und ist nicht so einge- schränkt wie die PHP-Funktionen. Da die Klasse sehr umfangreich ist, werde ich hier nicht alle Methoden und Funk- tionalitäten erläutern. Interessant ist aber vielleicht noch, dass Sie auch den Son- nenaufgang und Sonnenuntergang an bestimmten Orten berechnen können. Informationen dazu entnehmen Sie bitte der Dokumentation. 8.5.1 Ableiten eines Zend_Date-Objekts Wenn Sie ein Zend_Date-Objekt ableiten wollen, ist zu überlegen, welche Datums- und Zeitinformationen in dem Objekt enthalten sind bzw. sein sollen. Leiten Sie mit new ein Objekt ab, wird diesem die aktuelle Zeitinformation des Servers zugewiesen. Gleiches geschieht, wenn Sie die statische Methode now() aufrufen, die sicherlich besser zu lesen ist. Die beiden folgenden Zeilen haben also den gleichen Effekt: $date = new Zend_Date(): $date = Zend_Date::now(); Einen kleinen Vorteil hat die Nutzung von now() allerdings noch. Und zwar kön- nen Sie der Methode gleich ein Zend_Local e-Objekt oder einen Locale-Code mit- geben. Damit wird dann zwar immer noch die lokale Zeit des Servers genutzt, aber die Darstellung von Datum und Uhrzeit wird an die regionalen Vorgaben angepasst. Haben Sie das Objekt auf konventionellem Weg abgeleitet, können Sie die Lokalisierung nachträglich mit setLocaleO vornehmen. Die nachfolgend abgeleiteten Objekte würden somit alle die gleiche Lokalisierung aufweisen: $date = Zend_Date::now(’enJJS*); Slocale = new Zend_Locale('en_US'): $date2 = Zend_Date::now($locale): $date3 = new Zend_Date(): $date3->setLocale($locale); 395
e-bol.net 8 | Lokalisierung und Internationalisierung Auch der Methode setLocale() hätte man natürlich einen Lokalisierungs-String übergeben können. Sollten Sie sich einmal dafür interessieren, welche Lokalisie- rungseinstellung ein Objekt aufweist, so können Sie das mit getLocale() ausle- sen. Wie in diesem Fall sind auch die meisten anderen Methoden »in beide Rich- tungen« deklariert. Das bedeutet, dass Sie mit der set-Methode immer einen bestimmten Wert setzen und mit der entsprechenden get-Methode wieder aus- lesen können. Zeitzonen Wahrscheinlich werden Sie in Ihren Anwendungen relativ wenig mit verschiede- nen Zeitzonen zu tun haben. Sollten Sie damit aber arbeiten müssen, ist das kein Problem. Zend_Date unterstützt die Nutzung von Zeitzonen, und die meisten Methoden nutzen intern die Zeitzonen. Bevor ich zu dem komme, was Zend_Date in diesem Zusammenhang leistet, noch ein Wort zu Zeitzonen. Um der Tatsache, dass die Sonne wandert, gerecht zu werden, hat man auf der Welt verschiedene Zeitzonen eingeführt. Die Bezugs- größe dabei ist immer die UTC (Universal Time Coordinated) bzw. früher GMT (Greenwich Mean Time). Aus historischen Gründen ist die GMT anhand des Nullmeridians definiert, der durch London läuft. Bei allen Orten, die westlich von London liegen, wird daher Zeit »abgezogen«, und bei allen Orten, die östlich liegen, wird Zeit »dazugerechnet«. Das hat zur Folge, dass es in Deutschland eine Stunde später ist als zum gleichen Zeitpunkt in London, weil die Sonne hier schon weiter gewandert ist. Zum gleichen Zeitpunkt ist es in den USA - abhängig von der Zeitzone - allerdings etwa sieben Stunden früher. Oder mit konkreten Zahlen: Wenn es in London 12:00 Uhr ist, dann ist es in Deutschland bereits 13:00 Uhr und in den USA erst 5:00 Uhr morgens. Was heißt das nun für Zend_Date? Die Klasse übernimmt beim Ableiten des Objekts die Zeitzone, die für das System eingestellt ist. Daher sollten Sie diese immer korrekt mit der PHP-Funktion date_defaul t_timezone_set( )setzen. Um die Zeitzone des Zend_Date-Objekts zu verändern, können Sie die Methode setTimezone() nutzen, die den Namen einer Zeitzone übergeben bekommt. Die bekannten Zeitzonen können Sie der PHP-Dokumentation unter http://de.php. net/manual/de/timezones.php entnehmen. Die aktuelle Zeitzone lesen Sie mit getTimezone() aus, und den zeitlichen Unter- schied zwischen GMT und der aktuellen Zeitzone liefert Ihnen die Methode getGmtOffset() in Sekunden: 396
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 requi re_once('Zend/Date.php'); // Zeitzone korrekt setzen date_default_timezone_set('Europe/Berlin'): $date = Zend_Date::now(); echo "Aktuelle Zeit: ".$date; echo "<br>Zeitzone: ".$date->getTimezone(); echo "<br>Unterschied zu UTC: ".$date->getGmtOffset(): // Zeitzone neu setzen $date->setTimezone('America/New_York’); echo "<p>Aktuelle Zeit in New York: ".$date; echo "<br>Zeitzone: ".$date->getTimezone(); echo "<br>Unterschied zu UTC: ".$date->getGmtOffset(): Listing 8.12 Nutzung von Zeitzonen http7/127.0.0.1/~c . n/zf-buch/date2.php CD W W Ä Aktuelle Zeit: 17.01.2008 12:09:11 Zeitzone: Europc/Berlin Unterschied zu UTC: -3600 Aktuelle Zeit in New York: 17.01.2008 06:09:11 Zeitzone: Amcrica/New_York Unterschied zu UTC: 18000 Abbildung 8.7 Unterschiedliche Zeitzonen zum gleichen Zeitpunkt Ausgabe von Daten Bei der Nutzung eines Objekts stellt sich die Frage, wie Sie die enthaltenen Infor- mationen wieder auslesen können. Im einfachsten Fall geben Sie das Objekt direkt mit echo aus. Da in der Klasse die magische Methode_toStri ng() im- plementiert ist, wird das Datum automatisch in einen String konvertiert. Möchten Sie den Wert einer Variablen zuweisen, können Sie die Daten auch explizit mit (string) konvertieren. Sollten Sie die gesamten Datumsinformationen in Form eines assoziativen Arrays bevorzugen, so erhalten Sie dies, indem Sie die Methode toArrayO aufrufen. Dabei sind alle Informationen in den Feldern ’day', 'month', ’year', ’hour’, ’minute', ’second’, 'timezone', ’timestamp’, 'weekday', ’dayofyear', ’week’ und 'gmtsecs' enthalten. 397
e-bol.net 8 | Lokalisierung und Internationalisierung Eine andere, etwas komfortablere Vorgehensweise ist die Nutzung der Methode get(). Rufen Sie diese ohne Parameter auf, liefert Sie die Zeitinformation als Timestamp zurück. Sie können ihr allerdings mit dem ersten Parameter mitteilen, welche Information Sie auslesen wollen, bzw. in welchem Format diese ausgege- ben werden soll. Dafür ist eine beachtliche Anzahl von Konstanten definiert, von denen Sie einen Teil in Tabelle 8.6 finden. Mit dem zweiten Parameter können Sie optional noch eine Lokalisierung übergeben, sofern das für die Ausgabe not- wendig sein sollte. // Neues Datumsobjekt mit // amerikanischer Lokalisierung $date = Zend_Date::now('en_US'); // Normale Ausgabe echo $date. "<br>"; // "Langes Datum" in amerikanischer Formatierung echo $date->get(Zend_Date::DATE_FULL)."<br>"; // "Langes Datum" in deutscher Formatierung echo $date->get(Zend_Date::DATE_FULL, 'de_DE')."<br>"; // "Kurzes Datum" in amerikanischer Formatierung echo $date->get(Zend_Date::DATE_SHORT)."<br>”; // "Langes Datum" in deutscher Formatierung echo $date->get(Zend_Date::DATE_SHORT, ’de_DE')."<br>"; Listing 8.13 Verschiedene Datumsformate In diesem Beispiel würde die Ausgabe folgendermaßen aussehen: Jan 5. 2008 3:22:27 PM Saturday, January 5, 2008 Samstag, 5. Januar 2008 1/5/08 05.01.08 Die Anzahl der Konstanten, die Sie als ersten Parameter nutzen können, ist sehr umfangreich. Daher werde ich hier nur die wichtigsten erwähnen. Eine kom- plette Liste entnehmen Sie bitte der Dokumentation unter http://framework. zend.com/manual/de/zend.date.constants.html. 398
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 Konstante Bedeutung DATE_FULL Datum mit ausgeschriebenem Tag und Monat (Beispiel: Samstag, 5. Januar 2008) DATE_LONG Datum mit ausgeschriebenem Monat (Beispiel: 5. Januar 2008) DATE_MEDIUM Datum in der »üblichen« Schreibweise, Monat und Tag zweistellig (Beispiel: 05.01.2008) DATE_SHORT Tag, Monat und Jahr zweistellig (Beispiel: 05.01.08) TIMES Uhrzeit in der üblichen, lokalisierten Schreibweise (Beispiel: 15:42:11 oder 3:42:11 PM) TIME_L0NG Wie TIMES allerdings mit Zeitzone TIME_SHORT lokalisierte Uhrzeit mit Stunden und Minuten (Beispiel: 15:42 oder 3:42 PM) DAY Tag des Monats, zweistellig DAY_SHORT Tag des Monats mit ein oder zwei Stellen MONTH_NAME ausgeschriebener Name des Monats in der Sprache der Lokalisierung (Beispiel: January oder Januar) MONTH_NAME_SHORT abgekürzter Name des Monats in der Landessprache (Beispiel: Jan) MONTH Monat, zweistellig M0NTH_SH0RT AAonat, ein oder zweistellig YEAR Jahr, vierstellig YEAR_SHORT Jahr, zweistellig HOUR Stunden im 24-Stunden-Format, zweistellig HOUR_SHORT Stunden im 24-Stunden-Format, ein- oder zweistellig H0UR_AM Stunden im 12-Stunden-Format, zweistellig H0UR_SH0RT_AM Stunden im 12-Stunden-Format, ein- oder zweistellig MER I DI EM Vor- oder Nachmittag in der gewählten Lokalisierung (Beispiel: nachm., AAA oder PAA) MINUTE AAinuten, zweistellig MINUTE_SHORT AAinuten, ein- oder zweistellig SECOND Sekunden, zweistellig SECOND_SHORT Sekunden, ein - oder zweistellig I S0_8601 vollständige Datumsangabe im ISO 8601-Format (Beispiel: 2008-01-05T15:58:46+01:00) RFC_2822 Datumsangabe nach RFC 2822 (Internet AAessage Format) (Beispiel: Sat, 05 Jan 2008 16:00:41 +0100) TIMESTAMP Unix Timestamp Tabelle 8.6 Konstanten zur Nutzung mit Zend_Date 399
e-bol.net 8 | Lokalisierung und Internationalisierung Konstante Bedeutung ATOM Datum für die Nutzung in ATOM-Feeds (Beispiel: 2008-01-05T16:01:48+01:00) RFC_822 Datumsangabe nach RFC 822 (ARPA Internet Text AAessages) (Beispiel: Sat, 05 Jan 08 15:59:21 +0100) RFC_850 Datumsangabe nach RFC 850 (Interchange of USENET AAessages) (Beispiel: Saturday, 05-Jan-08 16:02:59 Europe/Berlin) RFC_1036 Datum nach RFC 1036 (Interchange of USENET AAessages; Nachfolger von RFC 850) (Beispiel: Sat, 05 Jan 08 16:03:59 +0100) RFC_1123 Datum nach RFC 1123 (Requirements for Internet Hosts) (Beispiel: Sat, 05 Jan 2008 16:09:24 +0100) RSS Datum zur Nutzung in RSS-Feeds (Beispiel: Sat, 05 Jan 2008 16:11:17 +0100) W3C Datum im W3C-Format zur Verwendung in HTAAL-Seiten (Beispiel: 2008-01-05T16:17:16+01:00) Tabelle 8.6 Konstanten zur Nutzung mit Zend_Date (Forts.) Sie sehen, die Methode ist wirklich sehr flexibel. Diese Konstanten können Sie übrigens auch noch bei einigen anderen Methoden nutzen, die Sie gleich ken- nenlernen werden. Die erste Methode, bei der Sie diese Konstanten wieder nutzen können, ist set(), mit der Sie eine Uhrzeit bzw. ein Datum setzen können. Das Spannende dabei ist, dass set() die oben genannten Formate verarbeiten kann und dadurch extrem flexibel ist: $date = Zend_Date::now('de_DE'); echo 'Aktuelles Datum: '.$date->get(Zend_Date::DATE_FULL).'<br>'; $date->set(’Mon, OZ Jan 08 16:03:59 +0100',Zend_Date::RFC_1036): echo 'Neu gesetztes Datum: '. $date->get(Zend_Date::DATE_FULL).’<br>'; $date->set('Dienstag',Zend_Date::WEEKDAY); echo 'Datum auf Dienstag gesetzt: '. $date->get(Zend_Date::DATE_FULL).’<br>'; $date->set('Sunday',Zend_Date::WEEKDAY,’en_US'): echo 'Datum auf Sonntag gesetzt: '. $date->get(Zend_Date::DATE_FULL).’<br>'; Listing 8.14 Setzen von Daten 400
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 In Abbildung 8.8 sehen Sie die generierte Ausgabe. http://127.0.0.1/.../zf-buch/datel.php O a « u Aktuelles Datum: Samstag. 5. Januar 2008 Neu gesetztes Datum: Montag, 7. Januar 2008 Datum auf Dienstag gesetzt: Dienstag, 8. Januar 2008 Datum auf Sonntag gesetzt: Sonntag, 6. Januar 2008 Abbildung 8.8 Ausgabe der Daten Eingehen möchte ich noch auf die letzten beiden Operationen. Hier wird der Tag zuerst auf Dienstag gesetzt, was dazu führt, dass das Datum auf den 08.01. korri- giert wird. Danach wird der Tag auf Sonntag gesetzt. Genauer gesagt auf Sunday, was kein Problem darstellt, da die Methode zusätzlich die Information bekom- men hat, dass das Datum in der amerikanischen Variante vorliegt. Interessant ist aber, dass das Datum danach der 06.01. ist und nicht der 13.01., wie man viel- leicht erwartet hätte. Das liegt daran, dass das Datum bei dieser Vorgehensweise immer auf einen Tag in der aktuellen Woche gesetzt wird. Und da die Woche nach ISO mit dem Sonntag beginnt, resultiert das in 06.01. Übrigens können Sie eine Zeitinformation mit nachfolgender Konstante und eventueller Lokalisierung auch direkt an den Konstruktor übergeben, falls Sie set() nicht nutzen wollen. Eine weitere Möglichkeit, das Datum zu setzen, ist, ein Array zu nutzen. Auch diese Vorgehensweise wird von beiden Methoden unterstützt. In dem Fall über- geben Sie die Datumsbestandteile in Form eines assoziativen Arrays, wobei Sie die Schlüssel day, month, year, hour, minute und second nutzen können, um den Tag, den Monat, das Jahr sowie die Stunden, Minuten und Sekunden zu überge- ben. Die Methoden set() und get() sind sehr flexibel und können die meisten Bedürfnisse ohne Probleme abdecken. Allerdings gibt es noch eine stattliche Anzahl von spezialisierten Methoden, die Sie in Tabelle 8.7 finden. Bei den hier beschriebenen get-Methoden ist allerdings ein Punkt zu beachten. getTimeO, getDatei) und getArpal) liefern einen String zurück. Die Methoden, die nur einen Teil des Datums auslesen, liefern allerdings ein Zend_Date-Objekt zurück, welches einen Timestamp enthält, das den entsprechenden Teil des Datums defi- niert. Wenn Sie beispielsweise davon ausgehen, dass Sie ein Objekt nutzen, das den 13.10.2008 als Grundlage hat, und lesen Sie den Monat mithilfe von get- 401
e-bol.net 8 | Lokalisierung und Internationalisierung Month() aus, so erhalten Sie ein Objekt, das den 01.10.1970 enthält. Das mag ein wenig ungewöhnlich erscheinen, aber es ist durchaus sinnvoll, weil Sie mit die- sen Datumsobjekten gut Weiterarbeiten können. Get-Methode Set-Methode Erläuterung Liest oder setzt... getTime() setT i me() ... die Zeit im TIMEJIEDIUM-Format getDate() setDate() ... das Datum im DATE_FULL-Format. Beide Methoden beachten dabei die Lokalisierung. getArpa() setArpa() ... ein Datum im RFC 822-Format getYear() setYear() ... das Jahr im vierstelligen Format getMontht) setMonth() ... den Monat getDay() setDay() ... den Tag im Monat getWeekday() setWeekday() ... den Wochentag getDayOfYear() setDayOfYear() ... den Tag im Jahr (1-365) getHour() setHour() ... die Stunden getMi nute() setMi nute() ... die Minuten getSecond() setSecond() ... die Sekunden getWeek() setWeek() ... die Wochennummer Tabelle 8.7 Methoden zum Lesen und Setzen von Daten 8.5.2 Rechnen mit Daten Nachdem Sie nun schon einiges über die Ein- und Ausgabe von Daten wissen, bleibt zu klären, wie Sie mit Daten und Zeiten rechnen. Auch das ist kein Pro- blem. Ähnlich wie beim Auslesen von Datumsinformationen stehen auch hier zwei allgemeine Methoden und eine recht große Anzahl von speziellen Metho- den zur Verfügung. Mit den Methoden add () und sub() können Sie addieren bzw. subtrahieren. Das heißt, zu der Zeitinformation, die in dem Objekt enthal- ten ist, wird eine bestimmte »Menge an Zeit« dazu addiert oder davon abgezo- gen: $date = Zend_Date::now('de_DE'): echo 'Aktuelles Datum: '.$date.'<br>'; $date->add(10, Zend_Date::HOUR); echo 'Datum plus 10 Stunden: '.$date.'<br>'; $date->add(1, Zend_Date::MONTH); echo 'Datum plus 10 Stunden und 1 Monat: '.$date.'<br>'; Listing 8.15 Addieren von Stunden 402
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 Wie Sie sehen, bekommen die Methoden einfach nur eine Zahl übergeben und danach die Information, um welche Zeiteinheit es sich handelt. Dabei können Sie wiederum auf die Konstanten aus Tabelle 8.6 zurückgreifen. Bei diesen Metho- den sind natürlich nur sinnvolle Konstanten zulässig. So können Sie zum Beispiel nicht MERIDIEM nutzen. Allerdings ist es möglich, dass Sie ein Datum zu einem anderen Datum addieren. Die folgenden Zeilen sind daher korrekt und funktio- nieren: $date = Zend_Date::now('de_DE'): $date->add('1.1.1971', Zend_Date::DATE_SHORT); Hierbei werden 1 Tag, 1 Monat und 1971 Jahre zum aktuellen Datum hinzu addiert. Ob eine solche Rechnung sinnvoll ist, ist eine andere Frage. Sinnvoller könnte es da schon sein, eine Uhrzeit, oder anders formuliert: eine Zeitspanne in Stunden, Minuten und Sekunden, zu addieren: $date->add('13:12:11', Zend_Date::TIMES): Die Methoden akzeptieren aber nicht nur Strings. Sie können ebenso gut ein Objekt der Klasse Zend_Date übergeben. Möchten Sie beispielsweise die Uhrzeit aus einem Objekt auslesen und ein anderes Datum mit der exakt gleichen Uhrzeit berechnen, können Sie das so lösen: $datel = new Zend_Date(’23.12.1999 1:23:11', 'de_DE'); $date2 = new Zend_Date('23.12.2000 1:00:00', 'de_DE'); echo 'Datum 1: '.$datel.'<br>'; echo 'Datum 2: '.$date2.’<br>'; $zeit = $datel->getTime(): echo 'Extrahierte Uhrzeit: '.$zeit.'<br>': $date2->add($zeit); echo 'Datum 2 mit Uhrzeit: ’.$date2; Listing 8.16 Addieren einer extrahierten Zeit http://127.O.O.l.. f-buch/datel.php GZ* JLWOO__________________________« Datum 1:23.12.1999 01:23:11 Datum 2: 23.12.2000 01:00:00 Extrahierte Uhrzeit: 01.01.1970 01:23:11 Datum 2 mit Uhrzeit: 23.122000 01:23:11 Abbildung 8.9 Ausgabe der berechneten Werte im Browser 403
e-bol.net 8 | Lokalisierung und Internationalisierung In diesem Beispiel mutet das zweite Datum vielleicht etwas merkwürdig an. Sicher hätten Sie eher erwartet, dass dort 00:00 Uhr oder gar keine Uhrzeit steht. Die Tatsache, dass hier 1:00 Uhr als Zeit genutzt wird, resultiert aus der Zeitzone Europe/Berlin, in der die Berechnung stattfindet. Sie liegt eine Stunde vor UTC bzw. GMT. Natürlich hätte man hier auch einfach die Anzahl der Sekunden, die getGmtOffset() liefert, addieren können. Bei Operationen dieser Art geht der Offset, der durch die Zeitzone definiert ist, verloren.5 add-Methode sub-Methode Beschreibung Addiert oder subtrahiert... addTimel) subTime() ... eine Uhrzeit (12:23:22). Achtung: Diese Methode kümmert sich selbst um den Offset von einer Stunde! addDatel) subDate() ... ein Datum (28.12.2008) addlsol) sublsoC) ... eine Datums- oder Zeitangabe im ISO-8601 Format addArpal) subArpa() ... eine Zeitinformation im ARPA-/ RFC-822-Format addYear() subYear() ... eine Jahreszahl. Zu zweistelligen Jahren zwischen 0 und 69 wird 2000 addiert, zu Jahren von 70 bis 99 wird 1900 addiert. Drei oder vierstellige Jahre werden direkt übernommen. addMonthl) subMonth() ... eine Monatszahl, wobei der Jahresübergang beachtet wird. addDayl) subDay() ... eine Anzahl Tage, wobei der Monatsübergang beachtet wird. addHourl) subHour() ... eine bestimmte Anzahl Stunden. Achtung: Die Methode beachtet selbstständig den Offset. addMi nute() subMi nute() ... die übergebene Anzahl an Minuten. add$econd() subSecondC) ... die übergebene Anzahl an Sekunden. Tabelle 8.8 Methoden zum Rechnen mit Zeiten Bei den Methoden addTime(), subTime(), addDate() und subDate() können Sie als zweiten Parameter eine Konstante übergeben, um ihr mitzuteilen, wie die Angabe formatiert ist. Ansonsten akzeptieren alle Methoden als letzten Parame- ter noch eine Information zur Lokalisierung. 8.5.3 Vergleich von Daten Arbeiten Sie öfter mit Daten oder Zeiten, so werden Sie das Problem kennen: Datums- oder Zeitinformationen zu vergleichen, ist oft recht umständlich. Wenn 5 Ich könnte mir vorstellen, dass dieses Verhalten sich in zukünftigen Versionen des Zend Frameworks ändert. Bitte prüfen Sie das Verhalten der Methode, falls Sie sie nutzen. 404
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 Sie aus dem Stegreif sagen sollten, ob 1000 Stunden mehr sind als 42 Tage, dürfte das sicher nicht ganz einfach sein. 42 Tage entsprechen übrigens 1008 Stunden. Auch hier gibt es wieder eine recht stattliche Anzahl an Methoden, die Ihnen zur Verfügung stehen. Die »Universalmethode«, die in diesem Fall deklariert ist, heißt compare(). Sie vergleicht den Wert, der in dem Objekt gespeichert ist, mit einer zweiten Datumsinformation, die als Parameter übergeben wird. Als Wert gibt Sie -1 zurück, wenn das Datum im Objekt kleiner ist, also zeitlich vor dem übergebenen Zeitpunkt liegt. Ist das übergebene Datum kleiner, dann ermittelt die Methode 1 als Ergebnis. Falls die beiden Zeiten gleich sind, liefert sie 0 als Ergebnis: Sdatei = new Zend_Date('12.12.1999', 'de_DE*); Sdate2 = new Zend_Date('23.12.2000 11:00:00’, ’de_DE'): Sergebnis = $datel->compare(Sdate2); switch (Sergebnis) { case -1: echo "Sdatei ist kleiner als Sdate2": break; case 1: echo "Sdatei ist größer als $date2”; break; case 0: echo "Sdatei und Sdate2 sind gleich"; } Listing 8.17 Vergleich von Zeiten Dieses kleine Listing ermittelt zielsicher, dass der Wert von Sdate2 größer ist als der von Sdatei. Somit erzeugt das Listing folgende Ausgabe: 12.12.1999 00:00:00 ist kleiner als 23.12.2000 11:00:00 Wichtig ist, dass Sie beim Vergleich von zwei Daten die Zeitzonen im Hinterkopf behalten. Die folgenden Daten sind gleich, wie Sie sehen werden, allerdings wer- den den Objekten unterschiedliche Zeitzonen zugewiesen: Sdatei = new Zend_Date('12.12.1999 11:00:00’, ’de_DE'); Sdatei->setTimezone(’Europe/Berli n'); Sdate2 = new Zend_Date('12.12.1999 11:00:00’, ’de_DE'); Sdate2->setTimezone(’Europe/London'); Werden diese beiden Objekte nun mittels der obigen Routine miteinander ver- glichen, würde folgendes Ergebnis ausgegeben: 12.12.1999 11:00:00 und 12.12.1999 10:00:00 sind gleich 405
e-bol.net 8 | Lokalisierung und Internationalisierung Das Ergebnis ist absolut korrekt. Allerdings fehlt hierbei die Information, dass es sich um denselben Zeitpunkt an unterschiedlichen Stellen der Welt, sprich unter- schiedliche Zeitzonen, handelt. Interessant ist auch, dass Sie der Methode als zweiten Parameter eine der Part- Konstanten übergeben können, falls Sie nur einen Teil der Information verglei- chen wollen. Wenn Sie also als zweiten Parameter Zend_Date::HOUR nutzen, werden lediglich die Stunden miteinander vergleichen. Eine sehr praktische Funktionalität, wie ich finde. Auch hier ist wieder eine stattliche Anzahl von spezialisierten Methoden dekla- riert. Diese finden Sie in der nächsten Tabelle. Methode Beschreibung Vergleicht.... compareTime() ... nur die Uhrzeiten, ignoriert das Datum. compareDateC) ... nur das Datum, ignoriert dabei die Zeit. comparelso() ... die Zeitinformation im ISO-Format, die ihr übergeben wurde, mit dem Datum im Objekt. compareArpaC) ... die Zeitinformation im ARPA-Format, die ihr übergeben wurde, mit dem Datum im Objekt. compareYear() ... die Jahreszahlen compareMonth() ...die Monate compareDay() ... die Tage im Monat (1 -31) compareWeekday() ... die Tage in der Woche (1 -7) compareDayOfYear() ... die Tage im Jahr (1-365) compareHour() ... die Stunden compareMi nute() ... die Minuten compareSecondC) ... die Sekunden compareWeekC) ... die Wochennummern compareTimestampC) ... den übergebenen Timestamp mit der enthaltenen Zeit Tabelle 8.9 Methoden zum Vergleich von Daten Die Methoden compareTime() und compareDate() akzeptieren neben der Zeit- bzw. Datumsinformation, die als erster Parameter übergeben wird, noch einen zweiten Parameter zur Beschreibung des Formats. Außerdem akzeptieren diese beiden sowie auch alle anderen Methoden noch einen weiteren Parameter, näm- lich die Lokalisierung. Bitte beachten Sie, dass Methoden, die lediglich einen Teil des Datums vergleichen, die Zeitzonen nicht beachten. 406
e-bol.net Datums- und Zeitangaben mit Zend_Date verarbeiten | 8.5 Zwei Methoden, die ein wenig aus der Reihe fallen, sind isEarl ier() und i sLa- te r(). Sie liefern einen booleschen Wert zurück, um Ihnen mitzuteilen, dass die übergebene Zeitinformation einen früheren oder späteren Zeitpunkt definiert als den, der im Objekt hinterlegt ist. Dabei unterstützen Sie dieselben Parameter wie compare(). 8.5.4 Prüfen von Datumsinformationen Die berühmte Frage, woran man ein Schaltjahr erkennt, kann auch nicht jeder Programmierer auf Anhieb beantworten. Daher kennt Zend_Date auch einige Möglichkeiten, um Daten zu testen. Um gleich beim Schaltjahr zu bleiben: Falls Sie herausfinden wollen, ob das Datum, das in einem Objekt hinterlegt ist, in einem Schaltjahr liegt, so können Sie die Methode isLeapYear() aufrufen. Sie liefert true zurück, falls es sich um ein Schaltjahr handelt. Alternativ können Sie auch die statische Methode checkLeapYeart) verwenden, die ein Datumsobjekt oder eine Jahreszahl übergeben bekommt und ebenfalls einen booleschen Wert zurückgibt. Ob das Datum, das in einem Objekt enthalten ist, dem heutigen Tag entspricht, kann i sToday() Ihnen sagen. Ob es sich um gestern oder morgen handelt, ermit- teln die Methoden i sYesterday() und i sTomorrow(). Sehr hilfreich ist auch die statische Methode i sDatet). Sie bekommt einen String übergeben, der eine Zeitinformation enthält bzw. enthalten könnte. Als zweiten Parameter übergeben Sie eine der Konstanten aus Tabelle 8.6. Optional können Sie auch noch eine Lokalisierungsinformation als dritten Parameter übergeben. Mittels true oder false teilt die Methode Ihnen mit, ob der übergebene String eine korrekte Zeitinformation darstellt. Das kann sehr hilfreich sein, falls Sie ein Datumsobjekt auf Basis dieses Strings ableiten und dabei keine Exception riskie- ren wollen. 407
e-bol.net
e-bol.net Inhalt der CD-ROM Auf der CD zu diesem Buch finden Sie neben allen Listings, die im Buch enthalten sind, auch noch weitere Software. Da wäre natürlich zuerst das Zend Framework zu nennen, das in der aktuellen Version enthalten ist. Bei Eclipse PDT handelt es sich um die bekannte Entwicklungsumgebung mit dem neuen PDT Plug-in. Hiermit erhalten Sie eine leistungsfähige, plattform- unabhängige Entwicklungsumgebung, die kostenlos genutzt werden darf. Sie bietet viele interessante Features wie zum Beispiel dient- und serverseitiges Debuggen. Neben Probekapiteln aus anderen Büchern ist auch das komplette Buch PHP PEAR von Carsten Möhrke enthalten, in dem Sie umfangreiche Informationen zum PEAR-Framework finden. 409
e-bol.net
e-bol.net Index A auslesen 350 getExpiryTime 348 Access Control List 115 Account-Authentication 250 ACL 115 Action Controller 30 Active Recordset 88 AJAX 306 Amazon 220 APC 137, 150 ASIN 225, 227 Assertion 121 Atom 209, 214 Authentifikation 124 dateibasierend 128 datenbankbasiert 124 AuthSub 250 AWS 220 isExpired 348 isSecure 348 isSession 348 setzen 350 D Datenbank 59 DELETE 77 DISTINCT 82 GROUPBY 82 HAVING 82 INSERT 76 JOIN 83 Konsistenz 103 LIMIT 81 Performance 110 B Prepared Statements 66 Profiling 110 Basename 168 Benutzerauthentifizierung 124 Bildersuche 242 Bogenmaß 326 Bootstrap-File 27 Breitengrad 362 Bugs 16 UPDATE 77 Datenbankunabhängigkeit 59 Datum 395 Ausgabe 397 prüfen 157, 407 rechnen mit 402 Vergleich von 404 Datum korrigieren 379 c Datumsangaben lokalisieren 378 Deklarative referentielle Integrität 103 Cache Einträge löschen 153 Garbage Collection 140 Caching 137 Dateien 147 Debugging 142 Gültigkeitsdauer 139 komplette Seiten 141 MVC 142 CC-Nummern 157 Chain 166 Changelog 16 Controller 25 Cookies 346 DIN-Datum 378 Dokumentation 15 Blogs 15 Wiki 15 DPI 308 DR1 103 E E-Mail-Adresse validieren 157 E-Mails 279 abholen 288 Anhänge 283 411
e-bol.net Index Betreff 280 Body 280 CC 281 Content-Disposition 284 Content-Type 284 Dringlichkeit 282 Expunge 298,301 Header 281 Header auslesen 292 HTML 282 Kopie 281 kopieren 304 löschen 298 Ordner 302 Papierkorb 302 Retum-Path 281 SMTP 286 Subject 280 Unique-IDs 299 verschieben 304 verschlüsseln 288 versenden 280 Exception Handling 21 Expunge 298 H Helper 49 hexadezimale Zahlen 160 htaccess 18 HTTP 335 Authentifizierung 341 Cookies 346 Datei-Download 345 Server-Antworten 342 SSL 352 Uploads 340 HTTP-Client 336 1 IIS 28 Installation 17 Internationalisierung 369 IP-Adressen validieren 163 Issue Tracker 16 J F JOIN 83 JSON 306 Feeds 209 finden 209 generieren 218 Filtern vonDaten 165 Flickr 232 Fließen, Maßeinheiten 386 Fluent Interface 23 Fluid Interface 23 Formulare validieren 175 Front Controller 29 K Kaskadierendes Löschen 109 Kochen, Maßeinheiten 386 Konfiguration 194 Kreditkartennummern 157 L Längengrad 362 lastlnsertld 75 G Laufweite 316 Locale 369 getPost 39 Glyphe 316 GMT 396 Google 245 Calendar 255 Spreadsheets 271 Logfiles 184 Lokalisierung 369 von Zahlen 373 Art m:n-Verknüpfung XE 108 Maildir 288 412
e-bol.net Index Mailing-Listen 15 Maßeinheiten 385 konvertieren von 388 rechnen mit 389 mbox 288 Mehrsprachigkeit 381 Memcached 150 MIME-Type 340 mod_rewrite 28 Model 26 Model View Controller -> MVC Module 33 MVC 25 ____call 43 Action Controller 30 assign 36 Beispiel 50 Caching 142 canSendHeaders 48 Controller 25 Controller Benunnung 32 Datenübergabe 36 dispatch 30 Error Handling 42 errorAction 45 ErrorController 45 escape 37 Exception Handling 30 Exceptions 30 Fortgeschrittene Techniken 47 forward 44 Front Controller 29 getParam 39 getPost 39 getRequest 38 getResponse 46 getStaticHelper 49 GET-Werte 38 Header 48 init 47 initView 36 mehrere Controller 32 Model 41 Module 33 Moved Permanently 45 Moved Temporarily 45 Parameter 39 Plug-ins 49 postDispatch 49 POST-Werte 38 preDispatch 49 redirect 45 Request-Objekt 38 setControllerDirectory 30, 31, 33 setParam 44 throwExceptions 30, 45 Übergabe von Werten 38 useDefaultControllerAlways 42 Verarbeitungsschritte 3 7 Verzeichnisstruktur 2 7 View 34 View unterdrücken 30 ViewRenderer 48 MX-Eintrag 158 N Natural Key 90 Negotiation 370 News-Suche 240 o One-Time-Use-Token 251 P Papierkorb 302 PDF 308 Bilder 330 Clipping 331 CMYK 320 Datei 312 drawLine 323 einlesen 333 Ellipse zeichnen 327 Farben 320 Fließtexte 315 Graustufen 321 Kreis zeichnen 326 laden 333 Meta-Informationen 331 Polygon zeichnen 328 Rechteck 324 RGB 320 Schriftarten 313 Schriftschnitt 314 Seiten hinzufugen 309 Text ausgeben 311 413
e-bol.net Index TrueType-Font 313 Zeichnen 322 PECL 151 Performance 110 php.ini 17 Plug-ins 49 preDispatch 49 u Übersetzen 371 Unique-IDs 299 URL-Rewriting 28 useDefaultControllerAlways 42 UTC 396 R V Realm 128 RealPath 170 Rechteverwaltung 115 referentielle Integrität 103 Ressourcen 116 REST 361 Client 362 Server 363 RewriteBase 29 Rewrite-Engine 28 Rewrite-Rule 28 RGB 320 Rollback 73 Rollen 116 RSS 209,211 Validierung 154 Vererbung von Rechten 118 Verzeichnisstruktur 27 View 25, 34 Speicherort 35 Übergabe von Werten 36 View-Helper 54 Viskosität, Maßeinheiten 386 w Währung Namen festlegen 391 Währungsdarstellung 3 89 Webinare 16 Websuche 236 s Whitespaces 171 Schaltjahr 407 Sequenzen 60, 75 Session beenden 132 Save-Handler 133 Schutz von Daten 132 Time-out 132 Sessions 129 SMTP after IMAP 287 SMTP after POP 287 SPL 24 Spreadsheets 271 SQLite 152 Standard Programmers Library 24 X xapi 247 XML-RPC 355 Client 359 Server 356 Y Yahoo! 236 Yahool-Maps 361 z Zeit 395 Ausgabe 397 rechnen mit 402 Tabellenkalkulation 271 Tags entfernen 172 to Lower 170 Transaktionen 73 Vergleich von 404 Zeitangaben lokalisieren 378 Zeitzonen 396 414
e-bol.net Index Zenc.Acl Frontend Output 140 isAllowed 118 Frontend Page 141 Zend Platform 152 Frontends 139 Zend.Acl 115 lifetime 139 add 118 load 148 addRole 117 Memchached 150 allow 118 MVC 142, 144 assert 121 remove 154 bekannte Rechte 123 Serialisierung 139 Bootstrap-File 123 start 138, 140 deny 118 Tags 153 getRoleld 117 write_control 139 Manipulieren von Rechten 123 Zend_Client_Http remove 123 Authentifizierung 341 removeAll 123 Zend_Config 194 removeAllow 123 Arrays 195 removeDeny 123 Default-Werte 196 removeRole 123 INI-Dateien 197 removeRoleAll 123 Konstante 199 Vererbung 118 Sektionen 199 Verfeinerung 120 SimpleXML 201 Zend.Auth 124, 127 XML-Dateien 200 authenticate 127 Zend_Console_Getopt 203 dateibasiert 128 Argumente 207 Datenbank 124 Cluster 204 MVC 129 Flag 203 setCredentialColumn 125 getRemainingArgs 207 setldentityColumn 125 getUserMessage 206 setTableName 125 Hilfe 207 Zend.Cache 137 setHelp 207 APC 150 Zend_Controller_Action 30 ausschalten 139 getResponse 46 automatiC-deaningfactor 140 Zend_Controller_Action_Exception 42 automatic-Serialization 139 Zend_Controller_Dispatcher_Exception Backend File 149 42 Backends 149 Zend_Controller_Front 27, 29 cacheByDefault 144 getlnstance 29 cachedEntity 146 setControllerDirectory 31, 33 cachedFunctions 145 setParam 30 caching 139 Zend_Controller_Request_Http 38 call 146 Zend_Currency 389 debug_header 142 Caching 394 Einträge löschen 153 getCurrencyList 394 end 138 getName 393 factory 138 getRegionList 394 Frontend Class 146 getSymbol 393 Frontend Core 149 name 391 Frontend File 147 setFormat 391 Frontend Function 144 toCurrency 390 415
e-bol.net Index Währungen 390 INSERT 76 Zend_Date 395 JOIN 83 add 402 lastlnsertld 75 checkLeapYear 407 HmitPage 81 compare 405 Optionen 61 compareDate 406 PDO-Optionen 62 compareTime 406 Platzhalter 65 get 398 Platzhalter, benannte 65 getArpa 401 prepare 66 getDate 401 quote 63 getGmtOffset 396 quoteColumnAs 64 getMonth 402 quoteldentißer 64 getTime 401 quotelnto 63 getTimezone 396 quoteTableAs 64 isDate 407 rollBack 73 isEarlier 407 rowCount 68 isLater 407 SELECT 78 isLeapYear 407 Sequenzen 75 isToday 407 setFetchMode 69 isTomorrow 407 Sortierung 82 isYesterday 407 Transaktionen 73 now 395 unterstützte Datenbanken 61 set 400 UPDATE 77 setLocale 395 WHERE 80 setTimezone 396 Zend_Db_Expr 94 sub 402 Zend_Db_Profile Zeitzonen 396 getLastQueryProßle 112 Zend_Db 59, 65 getQueryProßle 112 Aggregat-Funktionen 79 Zend_Db_Profiler 110 Aliasnamen 64 getProßler 110 auslesen von Daten 69 getTotalElapsedSecs 112 beginTransaction 73 getTotalNumQueries 112 Binding 67 setFilterElapsedSecs 113 bindParam 67 Zend_Db_Select 78 Case-Folding 62 Zend_Db_Table 88 DELETE 77 Abhängigkeiten 100 DISTINCT 82 alternative Syntax 108 execute 66 Einfügen von Daten 93 factory 6Q fetchAll 97 fetchAll 70 fetchRow 97 fetchAssoc 70 findDependentRowset 106 fetchCol 71 insert 93, 94 fetchColumn 71 Kaskadierendes Löschen 109 Fetch-Mode 69 LIMIT 97 fetchObject 71 Löschen von Daten 95 fetchOne 72 Natural Key 90 fetchPairs 72 onDelete 103 GROUP BY 82 onUpdate 103 HAVING 82 Primärschlüssel 90 416
e-bol.net Index save 98 SELECT 95 Sequenz 91 setFromArray 98 SetupTableName 89 Tabellenname 89 Zend_Db_Table_Row 95 Zend_Db_Table_Rowset 95 Zend_Feed 209 Atom 214 Attribute auslesen 211 Elemente auslesen 210 generieren von Feeds 218 import 210 importArray 218 RSS 211 Zend_Feed P Feeds Zend_Filter 165 Absoluter Pfad 170 alphanumerische Zeichen 167 Basename 168 Buchstaben filtern 168 Chain 166 eigene Filter 174 Entitäten 169 Großbuchstaben 171 HTML-Tags 172 Integer- Werte 170 Kleinbuchstaben 170 RealPath 170 setEncoding 170 Whitespaces 171 Ziffern 169 Zend_Filter_Input 175 Escape-Filter 179 Fehlermeldungen 179 getEscaped 178 getMessages 183 getMissing 183 getUnescaped 178 getUnknown 183 haslnvalid 183 hasUnknown 183 setDefaultEscapeFilter 179 Wildcard 176 Zend_Gdata 245 Account-Authentication 250 Allgemeines 246 Authentifikation 246 AuthSub 250 AuthSubRevokeToken 253 Calendar 255 CAPTCHA 248 delete 270 Erstellungsdatum 265 Event-Query 262, 265 eventStatus 260 Exception Handling 254 Feed 256 Geburtstage 269 gelöschte Termine 260 getAuthSubSessionToken 252 getAuthSubTokenUri 251 getCalendarEventEntry 270 getCalendarListFeed 261 getCaptchaToken 248, 250 getCaptchaUrl 248, 250 getCustom 273 getHttpClient 247, 249 getQueryUrl 267 getSpreadsheetFeed 271 insertEvent 267 insertRow 277 Kalendereintrag anlegen 265 Kalendereintragauslesen 270 Kalendereintrag löschen 270 Kalendereinträge ändern 269 magische Methoden 256 newContent 266 newTitle 266 One-Time-Use-Token 251 regelmäßige Termine 269 save 270 setProjection 262 setPublishedMax 265 setPublishedMin 265 setSpreadsheetQuery 276 setUpdatedMax 265 setUpdatedMin 265 setVisibility 262 Sortierreihenfolge 265 Spreadsheets auslesen 271 Structured Query 276 Suche 265 Tabellenblatt auslesen 273 Tabellenfelder auslesen 273 Tabellenzeile aktualisieren 278 Tabellenzeile löschen 277 417
e-bol.net Index Tabellenzeilen einfügen 277 Termin anlegen 265 updateRow 278 Veränderungsdatum 265 verschiedene Kalender 261 Volltextsuche 265 Zugriff auf Elemente 256 Zend_Gdata_Entry 256 Zend_Gdata_Feed 256 Zend.Http 335 Zend_Http_Client 336 Adapter 351 addCookie 350 Cookie setzen 350 Cookies 346 Datei-Download 345 getCookies 347 getHeader 345 getHeaders 345 getHeadersAsString 345 isError 343 isRedirect 343 isSuccessfull 343 Port 337 Proxy 351 Redirect 338 request 336 Server-Antworten 342 setAuth 341 setConfig 338 setCookieJar 347 setFileUpload 340 setParameterGet 339 setParameterPost 339 setUri 338 Uploads 340 User-Agent 338 Zend_Http_CookieJar 346 Zend_Http_Response 336 Zend_Json 306 decode 307 encode 306 Zend_Locale 369, 376 checkDate 380 getßrowser 370 getDate 378 getDateFormat 378 getFloat 376 getHttpCharset 371 getlnteger 376 getLanguage 370 getLanguageTranslation 373 getQuestion 373 getTranslationList 371 toFloat 374 tolnteger 376 toNumber 374 Zahlen 373 Zend_Log 184 addFilter 190 addWriter 186 CSV-Format 192 Datenbank 186 eigene Items 194 eigener Log-Level 189 Einträge filtern 190 Einträge formatieren 191 Einträge verwerfen 188 Filtern nach Priorität 191 Formatter 192 log 189 Loglevel 185 Mock-Writer 188 setEventltem 194 Stream-Writer 186 suppress 190 Writer 186 XML-Format 192 Zend_Mail 279 addBcc 281 addCc 281 addHeader 281 addTo 280 Anhänge 283 Anzahl der E-Mails 289 Attachments 283 Bcc 281 Betreff 280 Body 280 CC 281 Content-Disposition 284 Content-Type 284 copyMessage 305 countMessages 289 createAttachment 283 Dringlichkeit 282 E-Mails abholen 288 E-Mails löschen 298 418
e-bol.net Index E-Mails versenden 280 Expunge 298,301 Flag entfernen 302 Flags, IMAP 300 getContent 293 getCurrentFolder 304 getFolders 302 getHeader 292 getHeaders 293 getLocalName 302 getMessage 289 getNumberByUniqueld 299 getPart 293 getUniqueld 299 hasFlag 300 Header 281 Header auslesen 292 HTML-E-Mails 282 /MAP 288 IMAP, erweiterte Möglichkeiten 300 isMultipart 293 Kopie 281 kopieren von E-Mails 304 Ordner 302 Papierkorb 302 POP3 288 Recent-Flag 301 removeMessage 298 setBodyHtml 282 setBodyText 280 setFlags 300 setRetumPath 281 setSubject 280 setType 284 SMTP-Server 286 Subject 280 undelete 299 Unique-IDs 299 verschieben von E-Mails 304 Verschlüsseln 288 Zeichensatz 280 Zend_Measure 385 add 389 compare 389 convertTo 388 equals 389 getType 387 setValue 387 sub 389 Zend.Pdf 308 Bilder 330 Clipping 331 CMYK 320 Datei 312 DPI 308 drawCircle 326 drawEllipse 327 drawlmage 330 drawLine 323 drawPolygon 328 drawRectangle 324 drawText 311 einlesen 333 Ellipse 327 Farben 320 Fließtexte 315 fontWithName 311, 313 fontWithPath 313 Füllfarbe 321 Graustufen 321 imageWithPath 330 Kreis 326 laden 333 Linienfarbe 321 load 333 mehrzeilige Texte 315 Meta-Informationen 331 OpenType-Font 313 Polygon 328 Rechteck 324 render 312 RGB 320 save 312 Schriftart 311 Schriftarten 313 Schriftschnitt 314 Seiten hinzufugen 309 setFillColor 321 setFont 311 setLineColor 321 setLineDashingPattem 322 setLineWidth 322 setStyle 311 Stil 310 Stile 309 Text ausgeben 311 TrueType-Font 313 Zeichnen 322 419
e-bol.net Index Zend_Rest 361 get 362 MVC 365 post 362 Zend_Rest_Client setUri 362 Zend_Rest_Server 363 addFunction 364 Fehlerbehandlung 367 setClass 364 Zend_Service_Amazon 220 Antworten 228 ASIN 225 DetailPageURL 225 itemLookup 228 ItemSearch 221 Kategorien 222 Manufacturer 227 Offers 231 Response-Group 228 Rückgabewert 225 Searchlndex 221 Zend_Service_Flickr 232 firstResultPosition 233 Image-Objekt 234 Optionen 233 Seitenweises blättern 232 tagSearch 232 totalResultsAvailable 233 userSearch 232 Zend_Service_Yahoo 236 Bildersuche 242 imageSearch 242 News-Suche 240 searchNews 240 webSearch 236 Websuche 236 Zend_Session 129 expireSession Cookie 132 forgetMe 132 Namespaces 129 rememberMe 132 Save-Handler 133 Session starten 130 setSaveHandler 133 start 130 Time-out 132 Zend_Translate 381 Adapter 381 addTranslation 383 CSV-Dateien 384 Datenquellen 381 Delimiter 385 getMessagelds 384 Gettext 381 isAvailable 384 isTranslated 384 Mastersprache 383 Separator 385 setLocale 384 Zend_Uri 352 URI analysieren 354 Zend_Validate 154 alphanumerische Daten 156 Arrays 163 Datum 157 E-Mail-Adresse 157 Fehlermeldungen 155 Fließkomma 160 getMessages 154 Größer als 160 hexadezimale Zahlen 160 Hostnames 161 Integer-Werte 163 IP-Adressen 163 isValid 154 Kleiner als 164 Kreditkartennummern 157 regulärer Ausdruck 164 setMessage 155 String-Länge 165 Texte 156 Varaible 164 Zahlen 156 Ziffernfolge 157 Zend_View 34, 36 escape 37 Zend_View_Exception 34 Zend_XmlRpc 355 Fehlerbehandlung 358 setClass 357 system-Methoden 357 Zend_XmlRpc_Client 359 getLastRequest 361 ZFForum 16 ZFTutorials 16 Ziffern prüfen 157 420